using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; using Microsoft.VisualStudio; using Microsoft.VisualStudio.Shell; using Microsoft.VisualStudio.Shell.Interop; using Microsoft.VisualStudio.TextManager.Interop; using Task = System.Threading.Tasks.Task; namespace Prophet.CoopWithDsh { [PackageRegistration(UseManagedResourcesOnly = true, AllowsBackgroundLoading = true)] [InstalledProductRegistration("DSH Open File Bridge", "Publishes open file metadata for DeepSeek Harness", "0.1.0")] [ProvideAutoLoad(UIContextGuids80.NoSolution, PackageAutoLoadFlags.BackgroundLoad)] [ProvideAutoLoad(UIContextGuids80.SolutionExists, PackageAutoLoadFlags.BackgroundLoad)] [Guid(PackageGuidString)] public sealed class CoopWithDshPackage : AsyncPackage, IVsRunningDocTableEvents { public const string PackageGuidString = "1e02d547-499b-41ed-a25e-b356a9687e31"; private const int DebounceMilliseconds = 200; private const int HeartbeatMilliseconds = 15000; private IVsRunningDocumentTable? runningDocuments; private IVsSolution? solution; private SnapshotPublisher? publisher; private Timer? debounceTimer; private Timer? heartbeatTimer; private uint eventsCookie; private bool disposing; protected override async Task InitializeAsync(CancellationToken cancellationToken, IProgress progress) { await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); runningDocuments = await GetServiceAsync(typeof(SVsRunningDocumentTable)) as IVsRunningDocumentTable; solution = await GetServiceAsync(typeof(SVsSolution)) as IVsSolution; if (runningDocuments == null) throw new InvalidOperationException("Visual Studio Running Document Table is unavailable."); publisher = new SnapshotPublisher(); ErrorHandler.ThrowOnFailure(runningDocuments.AdviseRunningDocTableEvents(this, out eventsCookie)); debounceTimer = new Timer(_ => JoinableTaskFactory.RunAsync(PublishAsync), null, Timeout.Infinite, Timeout.Infinite); heartbeatTimer = new Timer(_ => JoinableTaskFactory.RunAsync(PublishAsync), null, HeartbeatMilliseconds, HeartbeatMilliseconds); AppDomain.CurrentDomain.ProcessExit += OnProcessExit; await PublishAsync(); } private void SchedulePublish() { if (!disposing) debounceTimer?.Change(DebounceMilliseconds, Timeout.Infinite); } private async Task PublishAsync() { if (disposing || publisher == null || runningDocuments == null) return; await JoinableTaskFactory.SwitchToMainThreadAsync(); var documents = EnumerateDocuments(runningDocuments); var workspace = ReadSolutionPath(solution); var snapshot = SnapshotModel.Create(publisher.InstanceId, Process.GetCurrentProcess().Id, workspace, documents, DateTimeOffset.UtcNow); try { publisher.Publish(snapshot); } catch (Exception error) { Trace.WriteLine($"DSH Open File Bridge publish failed: {error}"); } } private static IReadOnlyList EnumerateDocuments(IVsRunningDocumentTable table) { ThreadHelper.ThrowIfNotOnUIThread(); ErrorHandler.ThrowOnFailure(table.GetRunningDocumentsEnum(out var iterator)); var result = new List(); var cookies = new uint[1]; while (iterator.Next(1, cookies, out var fetched) == VSConstants.S_OK && fetched == 1) { IntPtr docData = IntPtr.Zero; try { ErrorHandler.ThrowOnFailure(table.GetDocumentInfo(cookies[0], out _, out _, out _, out var moniker, out _, out _, out docData)); if (string.IsNullOrWhiteSpace(moniker) || !Path.IsPathRooted(moniker) || !File.Exists(moniker)) continue; var dirty = false; if (docData != IntPtr.Zero) { var value = Marshal.GetObjectForIUnknown(docData); if (value is IVsPersistDocData persist && ErrorHandler.Succeeded(persist.IsDocDataDirty(out var isDirty))) dirty = isDirty != 0; } result.Add(new OpenDocument { Path = Path.GetFullPath(moniker), Label = Path.GetFileName(moniker), Dirty = dirty }); } catch (Exception error) { Trace.WriteLine($"DSH Open File Bridge skipped one document: {error.Message}"); } finally { if (docData != IntPtr.Zero) Marshal.Release(docData); } } return result; } private static string? ReadSolutionPath(IVsSolution? solutionService) { ThreadHelper.ThrowIfNotOnUIThread(); if (solutionService == null) return null; return ErrorHandler.Succeeded(solutionService.GetSolutionInfo(out _, out var solutionFile, out _)) && !string.IsNullOrWhiteSpace(solutionFile) ? solutionFile : null; } private void OnProcessExit(object sender, EventArgs e) => publisher?.Dispose(); protected override void Dispose(bool disposing) { this.disposing = true; if (disposing) { AppDomain.CurrentDomain.ProcessExit -= OnProcessExit; debounceTimer?.Dispose(); heartbeatTimer?.Dispose(); if (runningDocuments != null && eventsCookie != 0) { JoinableTaskFactory.Run(async () => { await JoinableTaskFactory.SwitchToMainThreadAsync(); runningDocuments.UnadviseRunningDocTableEvents(eventsCookie); }); } publisher?.Dispose(); } base.Dispose(disposing); } public int OnAfterFirstDocumentLock(uint docCookie, uint lockType, uint readLocks, uint editLocks) { SchedulePublish(); return VSConstants.S_OK; } public int OnBeforeLastDocumentUnlock(uint docCookie, uint lockType, uint readLocks, uint editLocks) { SchedulePublish(); return VSConstants.S_OK; } public int OnAfterSave(uint docCookie) { SchedulePublish(); return VSConstants.S_OK; } public int OnAfterAttributeChange(uint docCookie, uint attributes) { SchedulePublish(); return VSConstants.S_OK; } public int OnBeforeDocumentWindowShow(uint docCookie, int firstShow, IVsWindowFrame frame) { SchedulePublish(); return VSConstants.S_OK; } public int OnAfterDocumentWindowHide(uint docCookie, IVsWindowFrame frame) { SchedulePublish(); return VSConstants.S_OK; } } }