Skip to content

Commit ee310c7

Browse files
authored
Merge pull request #130 from sharwell/handle-cps-projects
Detect CPS opening project files from temporary locations
2 parents 348a1e3 + c25c3a7 commit ee310c7

7 files changed

Lines changed: 193 additions & 35 deletions

File tree

GitDiffMargin/Core/DiffUpdateBackgroundParser.cs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,9 @@ public class DiffUpdateBackgroundParser : BackgroundParser
1616
private readonly IGitCommands _commands;
1717
private readonly ITextDocument _textDocument;
1818
private readonly ITextBuffer _documentBuffer;
19+
private readonly string _originalPath;
1920

20-
internal DiffUpdateBackgroundParser(ITextBuffer textBuffer, ITextBuffer documentBuffer, TaskScheduler taskScheduler, ITextDocumentFactoryService textDocumentFactoryService, IGitCommands commands)
21+
internal DiffUpdateBackgroundParser(ITextBuffer textBuffer, ITextBuffer documentBuffer, string originalPath, TaskScheduler taskScheduler, ITextDocumentFactoryService textDocumentFactoryService, IGitCommands commands)
2122
: base(textBuffer, taskScheduler, textDocumentFactoryService)
2223
{
2324
_documentBuffer = documentBuffer;
@@ -26,11 +27,12 @@ internal DiffUpdateBackgroundParser(ITextBuffer textBuffer, ITextBuffer document
2627

2728
if (TextDocumentFactoryService.TryGetTextDocument(_documentBuffer, out _textDocument))
2829
{
29-
if (_commands.IsGitRepository(_textDocument.FilePath))
30+
_originalPath = originalPath;
31+
if (_commands.IsGitRepository(_textDocument.FilePath, _originalPath))
3032
{
3133
_textDocument.FileActionOccurred += OnFileActionOccurred;
3234

33-
var repositoryDirectory = _commands.GetGitRepository(_textDocument.FilePath);
35+
var repositoryDirectory = _commands.GetGitRepository(_textDocument.FilePath, _originalPath);
3436
if (repositoryDirectory != null)
3537
{
3638
_watcher = new FileSystemWatcher(repositoryDirectory);
@@ -100,7 +102,7 @@ protected override void ReParseImpl()
100102
ITextDocument textDocument;
101103
if (!TextDocumentFactoryService.TryGetTextDocument(_documentBuffer, out textDocument)) return;
102104

103-
var diff = _commands.GetGitDiffFor(textDocument, snapshot);
105+
var diff = _commands.GetGitDiffFor(textDocument, _originalPath, snapshot);
104106
var result = new DiffParseResultEventArgs(snapshot, stopwatch.Elapsed, diff.ToList());
105107
OnParseComplete(result);
106108
}

GitDiffMargin/Core/IMarginCore.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ internal interface IMarginCore
1414
event EventHandler<HunksChangedEventArgs> HunksChanged;
1515

1616
IWpfTextView TextView { get; }
17+
string OriginalPath { get; }
1718
IGitCommands GitCommands { get; }
1819
FontFamily FontFamily { get; }
1920
FontStretch FontStretch { get; }

GitDiffMargin/Core/MarginCore.cs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ internal sealed class MarginCore : IMarginCore, IDisposable
2727

2828
private bool _isDisposed;
2929

30-
public MarginCore(IWpfTextView textView, ITextDocumentFactoryService textDocumentFactoryService, IClassificationFormatMapService classificationFormatMapService, IEditorFormatMapService editorFormatMapService, IGitCommands gitCommands)
30+
public MarginCore(IWpfTextView textView, string originalPath, ITextDocumentFactoryService textDocumentFactoryService, IClassificationFormatMapService classificationFormatMapService, IEditorFormatMapService editorFormatMapService, IGitCommands gitCommands)
3131
{
3232
_textView = textView;
3333

@@ -38,7 +38,7 @@ public MarginCore(IWpfTextView textView, ITextDocumentFactoryService textDocumen
3838

3939
_gitCommands = gitCommands;
4040

41-
_parser = new DiffUpdateBackgroundParser(textView.TextBuffer, textView.TextDataModel.DocumentBuffer, TaskScheduler.Default, textDocumentFactoryService, GitCommands);
41+
_parser = new DiffUpdateBackgroundParser(textView.TextBuffer, textView.TextDataModel.DocumentBuffer, originalPath, TaskScheduler.Default, textDocumentFactoryService, GitCommands);
4242
_parser.ParseComplete += HandleParseComplete;
4343
_parser.RequestParse(false);
4444

@@ -59,6 +59,11 @@ public IWpfTextView TextView
5959
}
6060
}
6161

62+
public string OriginalPath
63+
{
64+
get;
65+
}
66+
6267
public IGitCommands GitCommands
6368
{
6469
get

GitDiffMargin/DiffMarginFactoryBase.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,12 +48,15 @@ protected IMarginCore TryGetMarginCore(IWpfTextViewHost textViewHost)
4848
if (fullPath == null)
4949
return null;
5050

51-
var repositoryPath = GitCommands.GetGitRepository(fullPath);
51+
if (!GitCommands.TryGetOriginalPath(fullPath, out string originalPath))
52+
return null;
53+
54+
var repositoryPath = GitCommands.GetGitRepository(fullPath, originalPath);
5255
if (repositoryPath == null)
5356
return null;
5457

5558
return textViewHost.TextView.Properties.GetOrCreateSingletonProperty(
56-
() => new MarginCore(textViewHost.TextView, TextDocumentFactoryService, ClassificationFormatMapService, EditorFormatMapService, GitCommands));
59+
() => new MarginCore(textViewHost.TextView, originalPath, TextDocumentFactoryService, ClassificationFormatMapService, EditorFormatMapService, GitCommands));
5760
}
5861

5962
private static string GetFullPath(string filename)

GitDiffMargin/Git/GitCommands.cs

Lines changed: 150 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,18 @@
55
using System.Linq;
66
using System.Text;
77
using LibGit2Sharp;
8+
using Microsoft.VisualStudio;
89
using Microsoft.VisualStudio.Shell;
910
using Microsoft.VisualStudio.Text;
1011
using __VSDIFFSERVICEOPTIONS = Microsoft.VisualStudio.Shell.Interop.__VSDIFFSERVICEOPTIONS;
12+
using __VSENUMPROJFLAGS = Microsoft.VisualStudio.Shell.Interop.__VSENUMPROJFLAGS;
13+
using IEnumHierarchies = Microsoft.VisualStudio.Shell.Interop.IEnumHierarchies;
1114
using IVsDifferenceService = Microsoft.VisualStudio.Shell.Interop.IVsDifferenceService;
15+
using IVsHierarchy = Microsoft.VisualStudio.Shell.Interop.IVsHierarchy;
16+
using IVsProject = Microsoft.VisualStudio.Shell.Interop.IVsProject;
17+
using IVsSolution = Microsoft.VisualStudio.Shell.Interop.IVsSolution;
1218
using SVsDifferenceService = Microsoft.VisualStudio.Shell.Interop.SVsDifferenceService;
19+
using SVsSolution = Microsoft.VisualStudio.Shell.Interop.SVsSolution;
1320

1421
namespace GitDiffMargin.Git
1522
{
@@ -26,10 +33,10 @@ public GitCommands(SVsServiceProvider serviceProvider)
2633

2734
private const int ContextLines = 0;
2835

29-
public IEnumerable<HunkRangeInfo> GetGitDiffFor(ITextDocument textDocument, ITextSnapshot snapshot)
36+
public IEnumerable<HunkRangeInfo> GetGitDiffFor(ITextDocument textDocument, string originalPath, ITextSnapshot snapshot)
3037
{
3138
var filename = textDocument.FilePath;
32-
var repositoryPath = GetGitRepository(Path.GetFullPath(filename));
39+
var repositoryPath = GetGitRepository(Path.GetFullPath(filename), ref originalPath);
3340
if (repositoryPath == null)
3441
yield break;
3542

@@ -39,7 +46,7 @@ public IEnumerable<HunkRangeInfo> GetGitDiffFor(ITextDocument textDocument, ITex
3946
if (workingDirectory == null)
4047
yield break;
4148

42-
var retrieveStatus = repo.RetrieveStatus(filename);
49+
var retrieveStatus = repo.RetrieveStatus(originalPath);
4350
if (retrieveStatus == FileStatus.Nonexistent)
4451
{
4552
// this occurs if a file within the repository itself (not the working copy) is opened.
@@ -52,9 +59,13 @@ public IEnumerable<HunkRangeInfo> GetGitDiffFor(ITextDocument textDocument, ITex
5259
yield break;
5360
}
5461

55-
if (retrieveStatus == FileStatus.Unaltered && !textDocument.IsDirty)
62+
if (retrieveStatus == FileStatus.Unaltered
63+
&& !textDocument.IsDirty
64+
&& Path.GetFullPath(filename) == originalPath)
5665
{
57-
// truly unaltered
66+
// Truly unaltered. The `IsDirty` check isn't valid for cases where the textDocument is a view of a
67+
// temporary copy of the file, since the temporary copy could have been made using unsaved changes
68+
// and still appear "not dirty".
5869
yield break;
5970
}
6071

@@ -63,7 +74,7 @@ public IEnumerable<HunkRangeInfo> GetGitDiffFor(ITextDocument textDocument, ITex
6374

6475
using (var currentContent = new MemoryStream(content))
6576
{
66-
var relativeFilepath = filename;
77+
var relativeFilepath = originalPath;
6778
if (relativeFilepath.StartsWith(workingDirectory, StringComparison.OrdinalIgnoreCase))
6879
relativeFilepath = relativeFilepath.Substring(workingDirectory.Length);
6980

@@ -139,20 +150,19 @@ private static byte[] GetCompleteContent(ITextDocument textDocument, ITextSnapsh
139150
return completeContent;
140151
}
141152

142-
public void StartExternalDiff(ITextDocument textDocument)
153+
public void StartExternalDiff(ITextDocument textDocument, string originalPath)
143154
{
144155
if (textDocument == null || string.IsNullOrEmpty(textDocument.FilePath)) return;
145156

146157
var filename = textDocument.FilePath;
147-
148-
var repositoryPath = GetGitRepository(Path.GetFullPath(filename));
158+
var repositoryPath = GetGitRepository(Path.GetFullPath(filename), ref originalPath);
149159
if (repositoryPath == null)
150160
return;
151161

152162
using (var repo = new Repository(repositoryPath))
153163
{
154164
string workingDirectory = repo.Info.WorkingDirectory;
155-
string relativePath = Path.GetFullPath(filename);
165+
string relativePath = originalPath;
156166
if (relativePath.StartsWith(workingDirectory, StringComparison.OrdinalIgnoreCase))
157167
relativePath = relativePath.Substring(workingDirectory.Length);
158168

@@ -174,7 +184,7 @@ public void StartExternalDiff(ITextDocument textDocument)
174184
IVsDifferenceService differenceService = _serviceProvider.GetService(typeof(SVsDifferenceService)) as IVsDifferenceService;
175185
string leftFileMoniker = tempFileName;
176186
// The difference service will automatically load the text from the file open in the editor, even if
177-
// it has changed.
187+
// it has changed. Don't use the original path here.
178188
string rightFileMoniker = filename;
179189

180190
string actualFilename = objectName;
@@ -207,7 +217,7 @@ public void StartExternalDiff(ITextDocument textDocument)
207217
leftLabel = string.Format("{0}@{1}", objectName, repo.Head.Tip.Sha.Substring(0, 7));
208218
}
209219

210-
string rightLabel = filename;
220+
string rightLabel = originalPath;
211221

212222
string inlineLabel = null;
213223
string roles = null;
@@ -220,26 +230,63 @@ public void StartExternalDiff(ITextDocument textDocument)
220230
}
221231

222232
/// <inheritdoc/>
223-
public bool IsGitRepository(string path)
233+
public bool TryGetOriginalPath(string path, out string originalPath)
224234
{
225-
return GetGitRepository(path) != null;
235+
originalPath = null;
236+
if (GetGitRepository(path, ref originalPath) == null)
237+
{
238+
originalPath = path;
239+
return false;
240+
}
241+
242+
return true;
226243
}
227244

228245
/// <inheritdoc/>
229-
public string GetGitRepository(string path)
246+
public bool IsGitRepository(string path, string originalPath)
247+
{
248+
return GetGitRepository(path, originalPath) != null;
249+
}
250+
251+
/// <inheritdoc/>
252+
public string GetGitRepository(string path, string originalPath)
253+
{
254+
if (originalPath == null)
255+
throw new ArgumentNullException(nameof(originalPath));
256+
257+
return GetGitRepository(path, ref originalPath);
258+
}
259+
260+
private string GetGitRepository(string path, ref string originalPath)
230261
{
231-
if (!Directory.Exists(path) && !File.Exists(path))
262+
if (originalPath == null)
263+
{
264+
originalPath = path;
265+
if (!Directory.Exists(path) && !File.Exists(path))
266+
return null;
267+
268+
var discoveredPath = Repository.Discover(Path.GetFullPath(path));
269+
if (discoveredPath != null)
270+
return discoveredPath;
271+
272+
originalPath = AdjustPath(path);
273+
if (originalPath == path)
274+
return null;
275+
}
276+
277+
if (!Directory.Exists(path) && !File.Exists(originalPath))
232278
return null;
233279

234-
var discoveredPath = Repository.Discover(Path.GetFullPath(path));
235-
// https://github.com/libgit2/libgit2sharp/issues/818#issuecomment-54760613
236-
return discoveredPath;
280+
return Repository.Discover(Path.GetFullPath(originalPath));
237281
}
238282

239283
/// <inheritdoc/>
240-
public string GetGitWorkingCopy(string path)
284+
public string GetGitWorkingCopy(string path, string originalPath)
241285
{
242-
var repositoryPath = GetGitRepository(path);
286+
if (originalPath == null)
287+
throw new ArgumentNullException(nameof(originalPath));
288+
289+
var repositoryPath = GetGitRepository(path, originalPath);
243290
if (repositoryPath == null)
244291
return null;
245292

@@ -282,5 +329,87 @@ static bool HasPreamble(string file, Encoding encoding)
282329

283330
return true;
284331
}
332+
333+
private string AdjustPath(string fullPath)
334+
{
335+
// Right now the only adjustment is for CPS-based project systems which open their project files in a
336+
// temporary location. There are several of these, such as .csproj, .vbproj, .shproj, and .fsproj, and more
337+
// could appear in the future.
338+
if (!fullPath.EndsWith("proj", StringComparison.Ordinal))
339+
{
340+
return fullPath;
341+
}
342+
343+
// CPS will open the file in %TEMP%\{random name}\{ProjectFileName}
344+
string directoryName = Path.GetDirectoryName(fullPath);
345+
if (string.IsNullOrEmpty(directoryName))
346+
return fullPath;
347+
348+
directoryName = Path.GetDirectoryName(directoryName);
349+
if (!Path.GetTempPath().Equals(directoryName + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase))
350+
return fullPath;
351+
352+
IVsSolution solution = _serviceProvider.GetService(typeof(SVsSolution)) as IVsSolution;
353+
if (solution == null)
354+
return fullPath;
355+
356+
if (!ErrorHandler.Succeeded(solution.GetProjectEnum((uint)__VSENUMPROJFLAGS.EPF_LOADEDINSOLUTION, Guid.Empty, out IEnumHierarchies ppenum))
357+
|| ppenum == null)
358+
{
359+
return fullPath;
360+
}
361+
362+
List<string> projectFiles = new List<string>();
363+
IVsHierarchy[] hierarchies = new IVsHierarchy[1];
364+
while (true)
365+
{
366+
int hr = ppenum.Next((uint)hierarchies.Length, hierarchies, out uint fetched);
367+
if (!ErrorHandler.Succeeded(hr))
368+
return fullPath;
369+
370+
for (uint i = 0; i < fetched; i++)
371+
{
372+
if (!(hierarchies[0] is IVsProject project))
373+
continue;
374+
375+
if (!ErrorHandler.Succeeded(project.GetMkDocument((uint)VSConstants.VSITEMID.Root, out string projectFilePath)))
376+
continue;
377+
378+
if (!Path.GetFileName(projectFilePath).Equals(Path.GetFileName(fullPath), StringComparison.Ordinal))
379+
continue;
380+
381+
projectFiles.Add(projectFilePath);
382+
}
383+
384+
if (hr != VSConstants.S_OK)
385+
{
386+
// No more projects
387+
break;
388+
}
389+
}
390+
391+
switch (projectFiles.Count)
392+
{
393+
case 0:
394+
// No matching project file found in solution
395+
return fullPath;
396+
397+
case 1:
398+
// Exactly one matching project file found in solution
399+
return projectFiles[0];
400+
401+
default:
402+
// Multiple project files found in solution; try to find one with a matching file size
403+
long desiredSize = new FileInfo(fullPath).Length;
404+
foreach (var projectFilePath in projectFiles)
405+
{
406+
if (File.Exists(projectFilePath) && new FileInfo(projectFilePath).Length == desiredSize)
407+
return projectFilePath;
408+
}
409+
410+
// No results found
411+
return fullPath;
412+
}
413+
}
285414
}
286415
}

0 commit comments

Comments
 (0)