129 lines
6.8 KiB
C#
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; }
|
||
|
|
}
|
||
|
|
}
|