From 7184b1569931c517320242f34e145dc71f23ad06 Mon Sep 17 00:00:00 2001 From: rtsummit Date: Mon, 18 May 2026 16:28:50 +0900 Subject: [PATCH 1/2] Add Visual Studio built-in diff option for Diff Against Have Revision Adds a "Use Visual Studio Diff" option (default on) that runs `p4 print` to fetch the have revision into a temp file, then opens the comparison in VS via IVsDifferenceService.OpenComparisonWindow2 instead of launching p4vc diffhave in P4V. Temp files are tracked in a static list and deleted on package Dispose, so they don't accumulate across the session. Users who prefer the existing P4VC diff window can turn the option off in Tools > Options > P4EditVS. Co-Authored-By: Claude Opus 4.7 (1M context) --- P4EditVS/Commands.cs | 145 +++++++++++++++++++++++++++++++++++++++++++ P4EditVS/P4EditVS.cs | 25 ++++++++ 2 files changed, 170 insertions(+) diff --git a/P4EditVS/Commands.cs b/P4EditVS/Commands.cs index 38a6310..5f3b041 100644 --- a/P4EditVS/Commands.cs +++ b/P4EditVS/Commands.cs @@ -639,6 +639,12 @@ private void ExecuteCommand(SelectedFile selectedFile, int commandId, bool immed case DiffCommandId: case CtxtDiffCommandId: { + if (_package.GetUseVisualStudioDiff()) + { + DiffAgainstHaveInVisualStudio(filePath, fileFolder, globalOptions, commandId); + return; + } + commandline = string.Format("p4vc {0} diffhave \"{1}\"", globalOptions, filePath); handler = CreateCommandRunnerResultHandler(GetBriefCommandDescription(commandId, filePath)); } @@ -901,6 +907,145 @@ private void SetStatusBarTextForRunnerResult(Runner.RunnerResult result, string // https://docs.microsoft.com/en-us/dotnet/standard/base-types/regular-expression-language-quick-reference private static readonly Regex AlsoOpenedByRegex = new Regex(@"^... (?:.*) - also opened by (?.*)$"); + // VSDIFFOPT flag value from Microsoft.VisualStudio.Shell.Interop.__VSDIFFSERVICEOPTIONS. + // Declared explicitly to avoid SDK-version differences between VS2019 / VS2022 builds. + private const uint VSDIFFOPT_LeftFileIsTemporary = 0x00000010; + + private void DiffAgainstHaveInVisualStudio(string filePath, string fileFolder, string globalOptions, int commandId) + { + ThreadHelper.ThrowIfNotOnUIThread(); + + string commandDescription = GetBriefCommandDescription(commandId, filePath); + + string tempFile; + try + { + // Preserve the original extension so VS picks the right diff content type. + string ext = Path.GetExtension(filePath); + tempFile = Path.Combine(Path.GetTempPath(), string.Format("P4EditVS_{0}_have{1}", Guid.NewGuid().ToString("N"), ext)); + } + catch (Exception ex) + { + OutputWindow.WriteLine("Diff: failed to build temp path: {0}", ex.Message); + SetStatusBarText(FAILURE_PREFIX + commandDescription, true); + return; + } + + string commandline = string.Format("p4 {0} print -q -o \"{1}\" \"{2}#have\"", globalOptions, tempFile, filePath); + string fileName = Path.GetFileName(filePath); + + Action handler = (Runner.RunnerResult result) => + { + if (!ShowRunnerResultOutput(result, commandDescription)) + { + SafeDeleteFile(tempFile); + return; + } + + if (result.ExitCode != 0 || !File.Exists(tempFile)) + { + SafeDeleteFile(tempFile); + SetStatusBarTextForRunnerResult(result, commandDescription); + return; + } + + try + { + OpenVisualStudioDiff(tempFile, filePath, fileName); + SetStatusBarText(SUCCESS_PREFIX + commandDescription, false); + } + catch (Exception ex) + { + OutputWindow.WriteLine("Diff: failed to open VS diff: {0}", ex); + SafeDeleteFile(tempFile); + SetStatusBarText(FAILURE_PREFIX + commandDescription, true); + } + }; + + var runner = Runner.Create(commandline, fileFolder, handler, null, null); + OutputWindow.WriteLine("{0}: started at {1}: {2}", runner.JobId, DateTime.Now, commandline); + Runner.Run(runner, true, _package.GetCommandTimeoutSeconds(), true); + } + + private void OpenVisualStudioDiff(string haveTempPath, string workspacePath, string fileName) + { + ThreadHelper.ThrowIfNotOnUIThread(); + + var diffService = ServiceProvider.GetService(typeof(SVsDifferenceService)) as IVsDifferenceService; + if (diffService == null) + { + throw new InvalidOperationException("SVsDifferenceService not available"); + } + + string leftLabel = fileName + "#have"; + string rightLabel = fileName; + string caption = string.Format("Diff - {0}", fileName); + string tooltip = string.Format("{0} vs {1}", leftLabel, workspacePath); + string inlineLabel = null; + string roles = null; + + diffService.OpenComparisonWindow2( + haveTempPath, + workspacePath, + caption, + tooltip, + leftLabel, + rightLabel, + inlineLabel, + roles, + VSDIFFOPT_LeftFileIsTemporary); + + // VSDIFFOPT_LeftFileIsTemporary is unreliable, and per-frame close + // hooks race with the diff editor still holding the file handle. + // Defer deletion until VS shuts down — by then all editors are gone. + RegisterDiffTempFile(haveTempPath); + } + + private static readonly List _diffTempFiles = new List(); + + private static void RegisterDiffTempFile(string path) + { + if (string.IsNullOrEmpty(path)) return; + lock (_diffTempFiles) + { + _diffTempFiles.Add(path); + } + } + + public static void CleanupDiffTempFiles() + { + List paths; + lock (_diffTempFiles) + { + paths = new List(_diffTempFiles); + _diffTempFiles.Clear(); + } + + foreach (var p in paths) + { + try + { + if (File.Exists(p)) File.Delete(p); + } + catch + { + // best effort — %TEMP% gets cleaned eventually + } + } + } + + private void SafeDeleteFile(string path) + { + try + { + if (path != null && File.Exists(path)) File.Delete(path); + } + catch (Exception) + { + // Best effort — VS will clean up on shutdown if marked temporary. + } + } + private void HandleCheckOutRunnerResult(Runner.RunnerResult result, string filePath) { ThreadHelper.ThrowIfNotOnUIThread(); diff --git a/P4EditVS/P4EditVS.cs b/P4EditVS/P4EditVS.cs index 7d351cf..69d9460 100644 --- a/P4EditVS/P4EditVS.cs +++ b/P4EditVS/P4EditVS.cs @@ -400,6 +400,15 @@ protected override async Task InitializeAsync(CancellationToken cancellationToke await Commands.InitializeAsync(this); } + protected override void Dispose(bool disposing) + { + if (disposing) + { + Commands.CleanupDiffTempFiles(); + } + base.Dispose(disposing); + } + private OptionPageGrid GetOptionsPage() { return (OptionPageGrid)GetDialogPage(typeof(OptionPageGrid)); @@ -533,6 +542,11 @@ public bool GetUseP4V2024_1_OpenInP4V() return GetOptionsPage().UseP4V2024_1_OpenInP4V; } + public bool GetUseVisualStudioDiff() + { + return GetOptionsPage().UseVisualStudioDiff; + } + #region Visual Studio suo interface @@ -813,6 +827,17 @@ public bool UseP4V2024_1_OpenInP4V set { _useP4V2024_1_OpenInP4V = value; } } + private bool _useVisualStudioDiff = true; + + [Category("Options")] + [DisplayName("Use Visual Studio Diff")] + [Description("Use the built-in Visual Studio diff viewer for 'Diff Against Have Revision' instead of launching p4vc diffhave in P4V. The have revision is fetched via 'p4 print' into a temporary file.")] + public bool UseVisualStudioDiff + { + get { return _useVisualStudioDiff; } + set { _useVisualStudioDiff = value; } + } + private string _userName = ""; private string _clientName = ""; private string _server = ""; From 5bb1152f5a945905071972cc2df138068a9658dc Mon Sep 17 00:00:00 2001 From: rtsummit Date: Mon, 18 May 2026 20:08:29 +0900 Subject: [PATCH 2/2] Update CI agent image: windows-2019 -> windows-latest The hosted windows-2019 image has been deprecated, causing the pipeline to fail with "No image label found to route agent pool" before the build even starts. Co-Authored-By: Claude Opus 4.7 (1M context) --- azure-pipelines.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 460fc7b..8fafc75 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -4,7 +4,7 @@ # https://docs.microsoft.com/azure/devops/pipelines/apps/windows/dot-net pool: - vmImage: 'windows-2019' + vmImage: 'windows-latest' variables: solution: '**/*.sln'