├── art ├── editor.png ├── diff-view.png ├── context-menu.png ├── multi-selection.png └── single-selection.png ├── src ├── Resources │ └── Icon.png ├── Properties │ └── AssemblyInfo.cs ├── ExtensionMethods.cs ├── source.extension.cs ├── source.extension.vsixmanifest ├── VSPackage.cs ├── Commands │ ├── UnmodifiedCommand.cs │ ├── DocumentSavedCommand.cs │ ├── FilesOnDiskCommand.cs │ ├── DocumentFileCommand.cs │ ├── DocumentClipboardCommand.cs │ ├── SelectionClipboardCommand.cs │ ├── ClipboardCommand.cs │ └── SelectedFilesCommand.cs ├── VSCommandTable.cs ├── FileDiffer.csproj └── VSCommandTable.vsct ├── .gitignore ├── SUPPORT.md ├── .gitattributes ├── LICENSE ├── appveyor.yml ├── FileDiffer.sln └── README.md /art/editor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madskristensen/FileDiffer/HEAD/art/editor.png -------------------------------------------------------------------------------- /art/diff-view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madskristensen/FileDiffer/HEAD/art/diff-view.png -------------------------------------------------------------------------------- /art/context-menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madskristensen/FileDiffer/HEAD/art/context-menu.png -------------------------------------------------------------------------------- /art/multi-selection.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madskristensen/FileDiffer/HEAD/art/multi-selection.png -------------------------------------------------------------------------------- /src/Resources/Icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madskristensen/FileDiffer/HEAD/src/Resources/Icon.png -------------------------------------------------------------------------------- /art/single-selection.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madskristensen/FileDiffer/HEAD/art/single-selection.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | packages 2 | 3 | # User files 4 | *.suo 5 | *.user 6 | *.sln.docstates 7 | .vs/ 8 | 9 | # Build results 10 | [Dd]ebug/ 11 | [Rr]elease/ 12 | x64/ 13 | [Bb]in/ 14 | [Oo]bj/ 15 | 16 | # MSTest test Results 17 | [Tt]est[Rr]esult*/ 18 | [Bb]uild[Ll]og.* 19 | 20 | # NCrunch 21 | *.ncrunchsolution 22 | *.ncrunchproject 23 | _NCrunch_WebCompiler -------------------------------------------------------------------------------- /SUPPORT.md: -------------------------------------------------------------------------------- 1 | # Support 2 | 3 | ## How to file issues and get help 4 | 5 | This project uses GitHub Issues to track bugs and feature requests. Please search the existing issues before filing new issues to avoid duplicates. For new issues, file your bug or feature request as a new Issue. 6 | 7 | For help and questions about using this project, please see the use Stack Overflow or reach me on [Twitter (@mkristensen)](https://twitter.com/mkristensen). 8 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | *.sln merge=union 7 | *.csproj merge=union 8 | *.vbproj merge=union 9 | *.fsproj merge=union 10 | *.dbproj merge=union 11 | 12 | # Standard to msysgit 13 | *.doc diff=astextplain 14 | *.DOC diff=astextplain 15 | *.docx diff=astextplain 16 | *.DOCX diff=astextplain 17 | *.dot diff=astextplain 18 | *.DOT diff=astextplain 19 | *.pdf diff=astextplain 20 | *.PDF diff=astextplain 21 | *.rtf diff=astextplain 22 | *.RTF diff=astextplain 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2016 Mads Kristensen 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. -------------------------------------------------------------------------------- /src/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.InteropServices; 3 | using FileDiffer; 4 | 5 | [assembly: AssemblyTitle(Vsix.Name)] 6 | [assembly: AssemblyDescription(Vsix.Description)] 7 | [assembly: AssemblyConfiguration("")] 8 | [assembly: AssemblyCompany(Vsix.Author)] 9 | [assembly: AssemblyProduct(Vsix.Name)] 10 | [assembly: AssemblyCopyright(Vsix.Author)] 11 | [assembly: AssemblyTrademark("")] 12 | [assembly: AssemblyCulture(Vsix.Language)] 13 | 14 | [assembly: ComVisible(false)] 15 | 16 | [assembly: AssemblyVersion(Vsix.Version)] 17 | [assembly: AssemblyFileVersion(Vsix.Version)] 18 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | image: Visual Studio 2022 2 | 3 | install: 4 | - ps: (new-object Net.WebClient).DownloadString("https://raw.github.com/madskristensen/ExtensionScripts/master/AppVeyor/vsix.ps1") | iex 5 | 6 | before_build: 7 | - ps: Vsix-IncrementVsixVersion | Vsix-UpdateBuildVersion 8 | - ps: Vsix-TokenReplacement src\source.extension.cs 'Version = "([0-9\\.]+)"' 'Version = "{version}"' 9 | 10 | build_script: 11 | - nuget restore -Verbosity quiet 12 | - msbuild /p:configuration=Release /p:DeployExtension=false /p:ZipPackageCompressionLevel=normal /v:m 13 | 14 | after_test: 15 | - ps: Vsix-PushArtifacts | Vsix-PublishToGallery 16 | -------------------------------------------------------------------------------- /src/ExtensionMethods.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using EnvDTE; 7 | 8 | namespace FileDiffer 9 | { 10 | internal static class ExtensionMethods 11 | { 12 | public static string GetText(this Document doc) 13 | { 14 | Microsoft.VisualStudio.Shell.ThreadHelper.ThrowIfNotOnUIThread(); 15 | var textDocument = (TextDocument)doc.Object(null); 16 | var startPoint = textDocument.StartPoint.CreateEditPoint(); 17 | return startPoint.GetText(textDocument.EndPoint); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/source.extension.cs: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------------ 2 | // 3 | // This file was generated by VSIX Synchronizer 4 | // Available from https://marketplace.visualstudio.com/items?itemName=MadsKristensen.VsixSynchronizer64 5 | // 6 | // ------------------------------------------------------------------------------ 7 | namespace FileDiffer 8 | { 9 | internal sealed partial class Vsix 10 | { 11 | public const string Id = "ea5c68d6-cdae-4e79-bd46-2a39e95bb256"; 12 | public const string Name = "File Differ"; 13 | public const string Description = @"The easiest way to diff two files directly in Solution Explorer"; 14 | public const string Language = "en-US"; 15 | public const string Version = "3.0"; 16 | public const string Author = "Mads Kristensen"; 17 | public const string Tags = "compare, files, diff"; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /FileDiffer.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.30313.93 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FileDiffer", "src\FileDiffer.csproj", "{6E40BA85-E02C-49AE-BA45-E5DDFAF59730}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{9E495E61-30E5-4399-9D0E-36388EA17151}" 9 | ProjectSection(SolutionItems) = preProject 10 | appveyor.yml = appveyor.yml 11 | README.md = README.md 12 | EndProjectSection 13 | EndProject 14 | Global 15 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 16 | Debug|Any CPU = Debug|Any CPU 17 | Release|Any CPU = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 20 | {6E40BA85-E02C-49AE-BA45-E5DDFAF59730}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {6E40BA85-E02C-49AE-BA45-E5DDFAF59730}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {6E40BA85-E02C-49AE-BA45-E5DDFAF59730}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {6E40BA85-E02C-49AE-BA45-E5DDFAF59730}.Release|Any CPU.Build.0 = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | GlobalSection(ExtensibilityGlobals) = postSolution 29 | SolutionGuid = {F7D43AEA-A06E-43BB-969A-69CCB45DCA28} 30 | EndGlobalSection 31 | EndGlobal 32 | -------------------------------------------------------------------------------- /src/source.extension.vsixmanifest: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | File Differ 6 | The easiest way to diff two files directly in Solution Explorer 7 | https://github.com/madskristensen/FileDiffer 8 | Resources\LICENSE 9 | Resources\Icon.png 10 | Resources\Icon.png 11 | compare, files, diff 12 | 13 | 14 | 15 | amd64 16 | 17 | 18 | arm64 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/VSPackage.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | using System.Threading; 4 | using Microsoft.VisualStudio; 5 | using Microsoft.VisualStudio.Shell; 6 | using Task = System.Threading.Tasks.Task; 7 | 8 | namespace FileDiffer 9 | { 10 | [PackageRegistration(UseManagedResourcesOnly = true, AllowsBackgroundLoading = true)] 11 | [Guid(PackageGuids.guidPackageString)] 12 | [InstalledProductRegistration(Vsix.Name, Vsix.Description, Vsix.Version)] 13 | [ProvideMenuResource("Menus.ctmenu", 1)] 14 | [ProvideAutoLoad(VSConstants.UICONTEXT.SolutionHasSingleProject_string, PackageAutoLoadFlags.BackgroundLoad)] 15 | [ProvideAutoLoad(VSConstants.UICONTEXT.SolutionHasMultipleProjects_string, PackageAutoLoadFlags.BackgroundLoad)] 16 | [ProvideAutoLoad(VSConstants.VsEditorFactoryGuid.TextEditor_string, PackageAutoLoadFlags.BackgroundLoad)] 17 | public sealed class FileDifferPackage : AsyncPackage 18 | { 19 | protected override async Task InitializeAsync(CancellationToken cancellationToken, IProgress progress) 20 | { 21 | await JoinableTaskFactory.SwitchToMainThreadAsync(); 22 | await SelectedFilesCommand.InitializeAsync(this); 23 | await FilesOnDiskCommand.InitializeAsync(this); 24 | await UnmodifiedCommand.InitializeAsync(this); 25 | await ClipboardCommand.InitializeAsync(this); 26 | await DocumentClipboardCommand.InitializeAsync(this); 27 | await SelectionClipboardCommand.InitializeAsync(this); 28 | await DocumentFileCommand.InitializeAsync(this); 29 | await DocumentSavedCommand.InitializeAsync(this); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Commands/UnmodifiedCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel.Design; 3 | using EnvDTE; 4 | using EnvDTE80; 5 | using Microsoft; 6 | using Microsoft.VisualStudio.Shell; 7 | using Task = System.Threading.Tasks.Task; 8 | 9 | namespace FileDiffer 10 | { 11 | internal sealed class UnmodifiedCommand 12 | { 13 | private static DTE2 _dte; 14 | 15 | public static async Task InitializeAsync(AsyncPackage package) 16 | { 17 | var commandService = await package.GetServiceAsync(typeof(IMenuCommandService)) as OleMenuCommandService; 18 | Assumes.Present(commandService); 19 | 20 | _dte = await package.GetServiceAsync(typeof(DTE)) as DTE2; 21 | Assumes.Present(_dte); 22 | 23 | var commandId = new CommandID(PackageGuids.guidDiffFilesCmdSet, PackageIds.Unmodified); 24 | var command = new OleMenuCommand(CommandCallback, commandId); 25 | command.BeforeQueryStatus += Command_BeforeQueryStatus; 26 | commandService.AddCommand(command); 27 | } 28 | 29 | private static void Command_BeforeQueryStatus(object sender, EventArgs e) 30 | { 31 | ThreadHelper.ThrowIfNotOnUIThread(); 32 | var command = (OleMenuCommand)sender; 33 | 34 | Command unmodified = _dte.Commands.Item("Team.Git.CompareWithUnmodified"); 35 | command.Enabled = unmodified?.IsAvailable == true; 36 | } 37 | 38 | private static void CommandCallback(object sender, EventArgs e) 39 | { 40 | ThreadHelper.ThrowIfNotOnUIThread(); 41 | Command command = _dte.Commands.Item("Team.Git.CompareWithUnmodified"); 42 | 43 | if (command != null && command.IsAvailable) 44 | { 45 | _dte.Commands.Raise(command.Guid, command.ID, null, null); 46 | } 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # File Differ 2 | 3 | [![Build status](https://ci.appveyor.com/api/projects/status/s65xx32188hpocy7?svg=true)](https://ci.appveyor.com/project/madskristensen/filediffer) 4 | 5 | Download this extension from the [VS Gallery](https://marketplace.visualstudio.com/items?itemName=MadsKristensen.FileDiffer) 6 | or get the [CI build](http://vsixgallery.com/extension/ea5c68d6-cdae-4e79-bd46-2a39e95bb256/). 7 | 8 | --------------------------------------- 9 | 10 | The easiest way to diff two files directly in solution explorer. This extension is inspired by a Visual Studio [feature request](https://developercommunity.visualstudio.com/t/is-there-a-way-to-compare-two-files-from-solution/619706), so please vote for it if you think it should be built in. 11 | 12 | ![Diff View](art/diff-view.png) 13 | 14 | ## Solution Explorer 15 | Here�s are the commands available from the right-click menu in Solution Explorer: 16 | 17 | * Compare two files in Solution Explorer 18 | * Compare file with another file on disks 19 | * Compare file with content of clipboard 20 | * Compare file with its unmodified version 21 | 22 | ### Compare selected files 23 | Select two files in Solution Explorer and right-click to bring up the context menu. 24 | 25 | ![Context Menu](art/multi-selection.png) 26 | 27 | Then select *Selected Files* to see them side-by-side in the diff view. 28 | 29 | ### Compare with a file on disk 30 | If you only selected a single file, a file selector prompt will show up to let you select which file on disk to diff against. 31 | 32 | ![Context Menu](art/single-selection.png) 33 | 34 | ### Compare with clipboard 35 | If there is text content on the clipboard, you can compare a file with it by selecting *Clipboard* from the context menu. 36 | 37 | ## Code editor 38 | There are also commands specific to the code editor. By right-clicking inside the code editor, you�ll get the following options for diffing: 39 | 40 | * Compare selection with clipboard 41 | * Compare active file with clipboard 42 | * Compare active file with saved 43 | * Compare active file with file on disk 44 | 45 | ![Context Menu](art/editor.png) 46 | 47 | ## License 48 | [Apache 2.0](LICENSE) -------------------------------------------------------------------------------- /src/Commands/DocumentSavedCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel.Design; 3 | using System.IO; 4 | using System.Text; 5 | using System.Windows.Forms; 6 | using EnvDTE; 7 | using EnvDTE80; 8 | using Microsoft; 9 | using Microsoft.VisualStudio.Shell; 10 | using Task = System.Threading.Tasks.Task; 11 | 12 | namespace FileDiffer 13 | { 14 | internal sealed class DocumentSavedCommand 15 | { 16 | private static DTE2 _dte; 17 | 18 | public static async Task InitializeAsync(AsyncPackage package) 19 | { 20 | var commandService = await package.GetServiceAsync(typeof(IMenuCommandService)) as OleMenuCommandService; 21 | Assumes.Present(commandService); 22 | 23 | _dte = await package.GetServiceAsync(typeof(DTE)) as DTE2; 24 | Assumes.Present(_dte); 25 | 26 | var commandId = new CommandID(PackageGuids.guidDiffFilesCmdSet, PackageIds.EditorBufferSaved); 27 | var command = new OleMenuCommand(CommandCallback, commandId); 28 | command.BeforeQueryStatus += Command_BeforeQueryStatus; 29 | commandService.AddCommand(command); 30 | } 31 | 32 | private static void Command_BeforeQueryStatus(object sender, EventArgs e) 33 | { 34 | ThreadHelper.ThrowIfNotOnUIThread(); 35 | var command = (OleMenuCommand)sender; 36 | 37 | command.Enabled = _dte.ActiveDocument?.Saved == false; 38 | } 39 | 40 | private static void CommandCallback(object sender, EventArgs e) 41 | { 42 | ThreadHelper.ThrowIfNotOnUIThread(); 43 | 44 | var ext = Path.GetExtension(_dte.ActiveDocument.FullName); 45 | var left = CreateTempFileFromClipboard(ext, File.ReadAllText(_dte.ActiveDocument.FullName)); 46 | var right = _dte.ActiveDocument.FullName; 47 | 48 | SelectedFilesCommand.Diff(left, right); 49 | File.Delete(left); 50 | } 51 | 52 | public static string CreateTempFileFromClipboard(string extension, string content) 53 | { 54 | var temp = Path.ChangeExtension(Path.GetTempFileName(), extension); 55 | File.WriteAllText(temp, content, Encoding.UTF8); 56 | return temp; 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/VSCommandTable.cs: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------------ 2 | // 3 | // This file was generated by VSIX Synchronizer 4 | // Available from https://marketplace.visualstudio.com/items?itemName=MadsKristensen.VsixSynchronizer64 5 | // 6 | // ------------------------------------------------------------------------------ 7 | namespace FileDiffer 8 | { 9 | using System; 10 | 11 | /// 12 | /// Helper class that exposes all GUIDs used across VS Package. 13 | /// 14 | internal sealed partial class PackageGuids 15 | { 16 | public const string guidPackageString = "6e490dec-1b23-471e-8120-f164af6b268a"; 17 | public static Guid guidPackage = new Guid(guidPackageString); 18 | 19 | public const string guidDiffFilesCmdSetString = "5034b97c-760a-45e5-a15d-d86dcfae06f7"; 20 | public static Guid guidDiffFilesCmdSet = new Guid(guidDiffFilesCmdSetString); 21 | 22 | public const string guidSolutionExplorerToolWindowString = "3ae79031-e1bc-11d0-8f78-00a0c9110057"; 23 | public static Guid guidSolutionExplorerToolWindow = new Guid(guidSolutionExplorerToolWindowString); 24 | 25 | public const string GitPackageString = "57735d06-c920-4415-a2e0-7d6e6fbdfa99"; 26 | public static Guid GitPackage = new Guid(GitPackageString); 27 | } 28 | 29 | /// 30 | /// Helper class that encapsulates all CommandIDs uses across VS Package. 31 | /// 32 | internal sealed partial class PackageIds 33 | { 34 | public const int FilesMenuGroup = 0x1020; 35 | public const int GitMenuGroup = 0x1030; 36 | public const int FlyoutMenu = 0x1040; 37 | public const int EditorFlyoutMenu = 0x1050; 38 | public const int EditorFlyoutMenuGroup = 0x1060; 39 | public const int DiffFilesCommandId = 0x0100; 40 | public const int Unmodified = 0x0110; 41 | public const int PreviousVersion = 0x0120; 42 | public const int Clipboard = 0x0130; 43 | public const int FileOnDisk = 0x0140; 44 | public const int EditorSelectionClipboard = 0x0150; 45 | public const int EditorBufferClipboard = 0x0160; 46 | public const int EditorBufferFile = 0x0170; 47 | public const int EditorBufferSaved = 0x0180; 48 | public const int GitEditorContextGroup = 0xE002; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Commands/FilesOnDiskCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.Design; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Windows.Forms; 7 | using EnvDTE; 8 | using EnvDTE80; 9 | using Microsoft; 10 | using Microsoft.VisualStudio.Shell; 11 | using Task = System.Threading.Tasks.Task; 12 | 13 | namespace FileDiffer 14 | { 15 | internal sealed class FilesOnDiskCommand 16 | { 17 | public static async Task InitializeAsync(AsyncPackage package) 18 | { 19 | var commandService = await package.GetServiceAsync(typeof(IMenuCommandService)) as OleMenuCommandService; 20 | Assumes.Present(commandService); 21 | 22 | var commandId = new CommandID(PackageGuids.guidDiffFilesCmdSet, PackageIds.FileOnDisk); 23 | var command = new OleMenuCommand(CommandCallback, commandId); 24 | command.BeforeQueryStatus += Command_BeforeQueryStatus; 25 | commandService.AddCommand(command); 26 | } 27 | 28 | private static void Command_BeforeQueryStatus(object sender, EventArgs e) 29 | { 30 | ThreadHelper.ThrowIfNotOnUIThread(); 31 | 32 | var command = (OleMenuCommand)sender; 33 | IEnumerable items = SelectedFilesCommand.GetSelectedFiles(); 34 | 35 | command.Visible = command.Enabled = items.Count() == 1; 36 | } 37 | 38 | private static void CommandCallback(object sender, EventArgs e) 39 | { 40 | ThreadHelper.ThrowIfNotOnUIThread(); 41 | 42 | if (CanFilesBeCompared(out var file1, out var file2)) 43 | { 44 | SelectedFilesCommand.Diff(file1, file2); 45 | } 46 | } 47 | 48 | private static bool CanFilesBeCompared(out string file1, out string file2) 49 | { 50 | ThreadHelper.ThrowIfNotOnUIThread(); 51 | IEnumerable items = SelectedFilesCommand.GetSelectedFiles(); 52 | 53 | file1 = null; 54 | file2 = items.ElementAtOrDefault(0); 55 | 56 | var dialog = new OpenFileDialog 57 | { 58 | InitialDirectory = Path.GetDirectoryName(file1) 59 | }; 60 | dialog.ShowDialog(); 61 | 62 | file1 = dialog.FileName; 63 | 64 | return !string.IsNullOrEmpty(file1) && !string.IsNullOrEmpty(file2); 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Commands/DocumentFileCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel.Design; 3 | using System.IO; 4 | using System.Text; 5 | using System.Windows.Forms; 6 | using EnvDTE; 7 | using EnvDTE80; 8 | using Microsoft; 9 | using Microsoft.VisualStudio.Shell; 10 | using Task = System.Threading.Tasks.Task; 11 | 12 | namespace FileDiffer 13 | { 14 | internal sealed class DocumentFileCommand 15 | { 16 | private static DTE2 _dte; 17 | 18 | public static async Task InitializeAsync(AsyncPackage package) 19 | { 20 | var commandService = await package.GetServiceAsync(typeof(IMenuCommandService)) as OleMenuCommandService; 21 | Assumes.Present(commandService); 22 | 23 | _dte = await package.GetServiceAsync(typeof(DTE)) as DTE2; 24 | Assumes.Present(_dte); 25 | 26 | var commandId = new CommandID(PackageGuids.guidDiffFilesCmdSet, PackageIds.EditorBufferFile); 27 | var command = new OleMenuCommand(CommandCallback, commandId); 28 | command.BeforeQueryStatus += Command_BeforeQueryStatus; 29 | commandService.AddCommand(command); 30 | } 31 | 32 | private static void Command_BeforeQueryStatus(object sender, EventArgs e) 33 | { 34 | ThreadHelper.ThrowIfNotOnUIThread(); 35 | var command = (OleMenuCommand)sender; 36 | 37 | command.Enabled = !string.IsNullOrEmpty(_dte.ActiveDocument?.FullName); 38 | } 39 | 40 | private static void CommandCallback(object sender, EventArgs e) 41 | { 42 | ThreadHelper.ThrowIfNotOnUIThread(); 43 | 44 | var dialog = new OpenFileDialog 45 | { 46 | InitialDirectory = Path.GetDirectoryName(_dte.ActiveDocument.FullName) 47 | }; 48 | 49 | if (dialog.ShowDialog() != DialogResult.OK) 50 | { 51 | return; 52 | } 53 | 54 | var ext = Path.GetExtension(_dte.ActiveDocument.FullName); 55 | var right = dialog.FileName; 56 | var left = CreateTempFileFromClipboard(ext, _dte.ActiveDocument.GetText()); 57 | 58 | SelectedFilesCommand.Diff(left, right); 59 | File.Delete(left); 60 | } 61 | 62 | public static string CreateTempFileFromClipboard(string extension, string content) 63 | { 64 | var temp = Path.ChangeExtension(Path.GetTempFileName(), extension); 65 | File.WriteAllText(temp, content, Encoding.UTF8); 66 | return temp; 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Commands/DocumentClipboardCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.Design; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Text; 7 | using System.Windows.Forms; 8 | using EnvDTE; 9 | using EnvDTE80; 10 | using Microsoft; 11 | using Microsoft.VisualStudio.ComponentModelHost; 12 | using Microsoft.VisualStudio.Shell; 13 | using Microsoft.VisualStudio.Text; 14 | using Microsoft.VisualStudio.Utilities; 15 | using Task = System.Threading.Tasks.Task; 16 | 17 | namespace FileDiffer 18 | { 19 | internal sealed class DocumentClipboardCommand 20 | { 21 | private static DTE2 _dte; 22 | 23 | public static async Task InitializeAsync(AsyncPackage package) 24 | { 25 | var commandService = await package.GetServiceAsync(typeof(IMenuCommandService)) as OleMenuCommandService; 26 | Assumes.Present(commandService); 27 | 28 | _dte = await package.GetServiceAsync(typeof(DTE)) as DTE2; 29 | Assumes.Present(_dte); 30 | 31 | var commandId = new CommandID(PackageGuids.guidDiffFilesCmdSet, PackageIds.EditorBufferClipboard); 32 | var command = new OleMenuCommand(CommandCallback, commandId); 33 | command.BeforeQueryStatus += Command_BeforeQueryStatus; 34 | commandService.AddCommand(command); 35 | } 36 | 37 | private static void Command_BeforeQueryStatus(object sender, EventArgs e) 38 | { 39 | ThreadHelper.ThrowIfNotOnUIThread(); 40 | var command = (OleMenuCommand)sender; 41 | 42 | command.Enabled = !string.IsNullOrEmpty(_dte.ActiveDocument?.FullName); 43 | } 44 | 45 | private static void CommandCallback(object sender, EventArgs e) 46 | { 47 | ThreadHelper.ThrowIfNotOnUIThread(); 48 | 49 | if (CanFilesBeCompared()) 50 | { 51 | var ext = Path.GetExtension(_dte.ActiveDocument.FullName); 52 | 53 | var left = CreateTempFileFromClipboard(ext, Clipboard.GetText(TextDataFormat.UnicodeText)); 54 | var right = _dte.ActiveDocument.FullName; 55 | 56 | SelectedFilesCommand.Diff(left, right); 57 | File.Delete(left); 58 | } 59 | } 60 | 61 | public static string CreateTempFileFromClipboard(string extension, string content) 62 | { 63 | var temp = Path.ChangeExtension(Path.GetTempFileName(), extension); 64 | File.WriteAllText(temp, content, Encoding.UTF8); 65 | return temp; 66 | } 67 | 68 | private static bool CanFilesBeCompared() 69 | { 70 | return !string.IsNullOrWhiteSpace(Clipboard.GetText(TextDataFormat.UnicodeText)); 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Commands/SelectionClipboardCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.Design; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Text; 7 | using System.Windows.Forms; 8 | using EnvDTE; 9 | using EnvDTE80; 10 | using Microsoft; 11 | using Microsoft.VisualStudio.ComponentModelHost; 12 | using Microsoft.VisualStudio.Shell; 13 | using Microsoft.VisualStudio.Text; 14 | using Microsoft.VisualStudio.Utilities; 15 | using Task = System.Threading.Tasks.Task; 16 | 17 | namespace FileDiffer 18 | { 19 | internal sealed class SelectionClipboardCommand 20 | { 21 | private static DTE2 _dte; 22 | 23 | public static async Task InitializeAsync(AsyncPackage package) 24 | { 25 | var commandService = await package.GetServiceAsync(typeof(IMenuCommandService)) as OleMenuCommandService; 26 | Assumes.Present(commandService); 27 | 28 | _dte = await package.GetServiceAsync(typeof(DTE)) as DTE2; 29 | Assumes.Present(_dte); 30 | 31 | var commandId = new CommandID(PackageGuids.guidDiffFilesCmdSet, PackageIds.EditorSelectionClipboard); 32 | var command = new OleMenuCommand(CommandCallback, commandId); 33 | command.BeforeQueryStatus += Command_BeforeQueryStatus; 34 | commandService.AddCommand(command); 35 | } 36 | 37 | private static void Command_BeforeQueryStatus(object sender, EventArgs e) 38 | { 39 | ThreadHelper.ThrowIfNotOnUIThread(); 40 | var command = (OleMenuCommand)sender; 41 | var selection = _dte.ActiveDocument?.Selection as TextSelection; 42 | 43 | command.Enabled = selection?.IsEmpty == false; 44 | } 45 | 46 | private static void CommandCallback(object sender, EventArgs e) 47 | { 48 | ThreadHelper.ThrowIfNotOnUIThread(); 49 | 50 | if (CanFilesBeCompared()) 51 | { 52 | var ext = Path.GetExtension(_dte.ActiveDocument.FullName); 53 | var selection = (TextSelection)_dte.ActiveDocument.Selection; 54 | 55 | var left = CreateTempFileFromClipboard(ext, selection.Text); 56 | var right = CreateTempFileFromClipboard(ext, Clipboard.GetText(TextDataFormat.UnicodeText)); 57 | 58 | SelectedFilesCommand.Diff(left, right); 59 | File.Delete(left); 60 | File.Delete(right); 61 | } 62 | } 63 | 64 | public static string CreateTempFileFromClipboard(string extension, string content) 65 | { 66 | var temp = Path.ChangeExtension(Path.GetTempFileName(), extension); 67 | File.WriteAllText(temp, content, Encoding.UTF8); 68 | return temp; 69 | } 70 | 71 | private static bool CanFilesBeCompared() 72 | { 73 | return !string.IsNullOrWhiteSpace(Clipboard.GetText(TextDataFormat.UnicodeText)); 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Commands/ClipboardCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.Design; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Text; 7 | using System.Windows.Forms; 8 | using Microsoft; 9 | using Microsoft.VisualStudio.ComponentModelHost; 10 | using Microsoft.VisualStudio.Shell; 11 | using Microsoft.VisualStudio.Text; 12 | using Microsoft.VisualStudio.Utilities; 13 | using Task = System.Threading.Tasks.Task; 14 | 15 | namespace FileDiffer 16 | { 17 | internal sealed class ClipboardCommand 18 | { 19 | public static async Task InitializeAsync(AsyncPackage package) 20 | { 21 | var commandService = await package.GetServiceAsync(typeof(IMenuCommandService)) as OleMenuCommandService; 22 | Assumes.Present(commandService); 23 | 24 | var commandId = new CommandID(PackageGuids.guidDiffFilesCmdSet, PackageIds.Clipboard); 25 | var command = new OleMenuCommand(CommandCallback, commandId); 26 | command.BeforeQueryStatus += Command_BeforeQueryStatus; 27 | commandService.AddCommand(command); 28 | } 29 | 30 | private static void Command_BeforeQueryStatus(object sender, EventArgs e) 31 | { 32 | ThreadHelper.ThrowIfNotOnUIThread(); 33 | var command = (OleMenuCommand)sender; 34 | IEnumerable items = SelectedFilesCommand.GetSelectedFiles(); 35 | 36 | command.Enabled = command.Visible = items.Count() == 1; 37 | } 38 | 39 | private static void CommandCallback(object sender, EventArgs e) 40 | { 41 | ThreadHelper.ThrowIfNotOnUIThread(); 42 | 43 | if (CanFilesBeCompared()) 44 | { 45 | var right = SelectedFilesCommand.GetSelectedFiles().FirstOrDefault(); 46 | 47 | if (!string.IsNullOrEmpty(right)) 48 | { 49 | Encoding encoding = GetEncoding(right); 50 | 51 | var left = Path.ChangeExtension(Path.GetTempFileName(), Path.GetExtension(right)); 52 | File.WriteAllText(left, Clipboard.GetText(TextDataFormat.UnicodeText), encoding); 53 | 54 | SelectedFilesCommand.Diff(left, right); 55 | File.Delete(left); 56 | } 57 | } 58 | } 59 | 60 | private static Encoding GetEncoding(string fileName) 61 | { 62 | ThreadHelper.ThrowIfNotOnUIThread(); 63 | var componentModel = (IComponentModel)ServiceProvider.GlobalProvider.GetService(typeof(SComponentModel)); 64 | Assumes.Present(componentModel); 65 | 66 | ITextDocumentFactoryService docService = componentModel.GetService(); 67 | IFileToContentTypeService contentTypeService = componentModel.GetService(); 68 | IContentType contentType = contentTypeService.GetContentTypeForExtension(Path.GetExtension(fileName)); 69 | 70 | using (ITextDocument doc = docService.CreateAndLoadTextDocument(fileName, contentType)) 71 | { 72 | return doc.Encoding; 73 | } 74 | } 75 | 76 | private static bool CanFilesBeCompared() 77 | { 78 | return !string.IsNullOrWhiteSpace(Clipboard.GetText(TextDataFormat.UnicodeText)); 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/Commands/SelectedFilesCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.Design; 4 | using System.Linq; 5 | using EnvDTE; 6 | using EnvDTE80; 7 | using Microsoft; 8 | using Microsoft.VisualStudio.Shell; 9 | using Microsoft.Win32; 10 | using Task = System.Threading.Tasks.Task; 11 | 12 | namespace FileDiffer 13 | { 14 | internal sealed class SelectedFilesCommand 15 | { 16 | private static DTE2 _dte; 17 | 18 | public static async Task InitializeAsync(AsyncPackage package) 19 | { 20 | var commandService = await package.GetServiceAsync(typeof(IMenuCommandService)) as OleMenuCommandService; 21 | Assumes.Present(commandService); 22 | 23 | _dte = await package.GetServiceAsync(typeof(DTE)) as DTE2; 24 | Assumes.Present(_dte); 25 | 26 | var commandId = new CommandID(PackageGuids.guidDiffFilesCmdSet, PackageIds.DiffFilesCommandId); 27 | var command = new OleMenuCommand(CommandCallback, commandId); 28 | command.BeforeQueryStatus += Command_BeforeQueryStatus; 29 | commandService.AddCommand(command); 30 | } 31 | 32 | private static void Command_BeforeQueryStatus(object sender, EventArgs e) 33 | { 34 | ThreadHelper.ThrowIfNotOnUIThread(); 35 | 36 | var command = (OleMenuCommand)sender; 37 | IEnumerable items = GetSelectedFiles(); 38 | 39 | command.Visible = command.Enabled = items.Count() == 2; 40 | } 41 | 42 | private static void CommandCallback(object sender, EventArgs e) 43 | { 44 | ThreadHelper.ThrowIfNotOnUIThread(); 45 | 46 | if (CanFilesBeCompared(out var file1, out var file2)) 47 | { 48 | Diff(file1, file2); 49 | } 50 | } 51 | 52 | public static void Diff(string left, string right) 53 | { 54 | ThreadHelper.ThrowIfNotOnUIThread(); 55 | 56 | if (!DiffFileUsingCustomTool(left, right)) 57 | { 58 | DiffFilesUsingDefaultTool(left, right); 59 | } 60 | } 61 | 62 | private static void DiffFilesUsingDefaultTool(string file1, string file2) 63 | { 64 | ThreadHelper.ThrowIfNotOnUIThread(); 65 | // This is the guid and id for the Tools.DiffFiles command 66 | var diffFilesCmd = "{5D4C0442-C0A2-4BE8-9B4D-AB1C28450942}"; 67 | var diffFilesId = 256; 68 | object args = $"\"{file1}\" \"{file2}\""; 69 | 70 | _dte.Commands.Raise(diffFilesCmd, diffFilesId, ref args, ref args); 71 | } 72 | 73 | //Visual Studio allows replacing the default diff tool with a custom one. 74 | //See, for example: 75 | //Using WinMerge: https://blog.paulbouwer.com/2010/01/31/replace-diffmerge-tool-in-visual-studio-team-system-with-winmerge/ 76 | //Using BeyondCompare: http://stackoverflow.com/questions/4466238/how-to-configure-visual-studio-to-use-beyond-compare 77 | private static bool DiffFileUsingCustomTool(string file1, string file2) 78 | { 79 | try 80 | { 81 | //Checking the registry to see if a custom tool is configured 82 | //Relevant information: https://social.msdn.microsoft.com/Forums/vstudio/en-US/37a26013-2f78-4519-85e5-d896ac27f31e/see-what-default-visual-studio-tfexe-compare-tool-is-set-to-using-visual-studio-api?forum=vsx 83 | var registryFolder = $"{_dte.RegistryRoot}\\TeamFoundation\\SourceControl\\DiffTools\\.*\\Compare"; 84 | 85 | using (RegistryKey key = Registry.CurrentUser.OpenSubKey(registryFolder)) 86 | { 87 | var command = key?.GetValue("Command") as string; 88 | if (string.IsNullOrEmpty(command)) 89 | { 90 | return false; 91 | } 92 | 93 | var args = key.GetValue("Arguments") as string; 94 | if (string.IsNullOrEmpty(args)) 95 | { 96 | return false; 97 | } 98 | 99 | //Understanding the arguments: https://msdn.microsoft.com/en-us/library/ms181446(v=vs.100).aspx 100 | args = 101 | args.Replace("%1", $"\"{file1}\"") 102 | .Replace("%2", $"\"{file2}\"") 103 | .Replace("%5", string.Empty) 104 | .Replace("%6", $"\"{file1}\"") 105 | .Replace("%7", $"\"{file2}\""); 106 | System.Diagnostics.Process.Start(command, args); 107 | } 108 | return true; 109 | } 110 | catch (Exception ex) 111 | { 112 | System.Diagnostics.Debug.Write(ex); 113 | return false; 114 | } 115 | } 116 | 117 | private static bool CanFilesBeCompared(out string file1, out string file2) 118 | { 119 | ThreadHelper.ThrowIfNotOnUIThread(); 120 | IEnumerable items = GetSelectedFiles(); 121 | 122 | file1 = items.ElementAtOrDefault(0); 123 | file2 = items.ElementAtOrDefault(1); 124 | 125 | return items.Count() == 2; 126 | } 127 | 128 | public static IEnumerable GetSelectedFiles() 129 | { 130 | ThreadHelper.ThrowIfNotOnUIThread(); 131 | var items = (Array)_dte.ToolWindows.SolutionExplorer.SelectedItems; 132 | 133 | return from item in items.Cast() 134 | let pi = item.Object as ProjectItem 135 | select pi.FileNames[1]; 136 | } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/FileDiffer.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | $(VisualStudioVersion) 5 | $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) 6 | true 7 | 8 | 9 | 10 | Program 11 | $(DevEnvDir)\devenv.exe 12 | /rootsuffix Exp 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | Debug 21 | AnyCPU 22 | 2.0 23 | {82b43b9b-a64c-4715-b499-d71e9ca2bd60};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} 24 | {6E40BA85-E02C-49AE-BA45-E5DDFAF59730} 25 | Library 26 | Properties 27 | FileDiffer 28 | FileDiffer 29 | v4.8 30 | true 31 | true 32 | true 33 | true 34 | true 35 | false 36 | 37 | 38 | true 39 | full 40 | false 41 | bin\Debug\ 42 | DEBUG;TRACE 43 | prompt 44 | 4 45 | 46 | 47 | pdbonly 48 | true 49 | bin\Release\ 50 | TRACE 51 | prompt 52 | 4 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | True 66 | True 67 | source.extension.vsixmanifest 68 | 69 | 70 | 71 | True 72 | True 73 | VSCommandTable.vsct 74 | 75 | 76 | 77 | 78 | 79 | Resources\LICENSE 80 | true 81 | 82 | 83 | Designer 84 | VsixManifestGenerator 85 | source.extension.cs 86 | 87 | 88 | 89 | 90 | Menus.ctmenu 91 | VsctGenerator 92 | VSCommandTable.cs 93 | 94 | 95 | 96 | 97 | Always 98 | true 99 | 100 | 101 | 102 | 103 | 17.0.32112.339 104 | compile; build; native; contentfiles; analyzers; buildtransitive 105 | 106 | 107 | 17.4.1111 108 | runtime; build; native; contentfiles; analyzers; buildtransitive 109 | all 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 126 | -------------------------------------------------------------------------------- /src/VSCommandTable.vsct: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | Compare 14 | 15 | 16 | 17 | 18 | 19 | Compare 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 46 | 55 | 65 | 75 | 85 | 93 | 101 | 108 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | --------------------------------------------------------------------------------