Files
dsh-plugin-coop-with-vs/visualstudio-extension/Prophet.CoopWithDsh/CoopWithDshPackage.cs
T

129 lines
6.8 KiB
C#

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<ServiceProgressData> 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<OpenDocument> EnumerateDocuments(IVsRunningDocumentTable table)
{
ThreadHelper.ThrowIfNotOnUIThread();
ErrorHandler.ThrowOnFailure(table.GetRunningDocumentsEnum(out var iterator));
var result = new List<OpenDocument>();
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; }
}
}