├── 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 |
17 |
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 | [](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 | 
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 | 
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 | 
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 | 
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 |
16 |
22 |
23 |
24 |
25 |
28 |
31 |
34 |
35 |
36 |
37 |
46 |
55 |
65 |
75 |
85 |
93 |
101 |
108 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
126 |
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 |
--------------------------------------------------------------------------------