Skip to content

Commit e540922

Browse files
committed
Support multi-root workspaces for CurrentFile.RelativePath
1 parent 6aa24d6 commit e540922

File tree

7 files changed

+63
-118
lines changed

7 files changed

+63
-118
lines changed

src/PowerShellEditorServices/Extensions/FileContext.cs

+1-2
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,7 @@ public sealed class FileContext
5858
/// <summary>
5959
/// Gets the workspace-relative path of the file.
6060
/// </summary>
61-
public string WorkspacePath => editorOperations.GetWorkspaceRelativePath(
62-
scriptFile.FilePath);
61+
public string WorkspacePath => editorOperations.GetWorkspaceRelativePath(scriptFile);
6362

6463
#endregion
6564

src/PowerShellEditorServices/Extensions/IEditorOperations.cs

+1-2
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,8 @@ internal interface IEditorOperations
2929
/// <summary>
3030
/// Resolves the given file path relative to the current workspace path.
3131
/// </summary>
32-
/// <param name="filePath">The file path to be resolved.</param>
3332
/// <returns>The resolved file path.</returns>
34-
string GetWorkspaceRelativePath(string filePath);
33+
string GetWorkspaceRelativePath(ScriptFile scriptFile);
3534

3635
/// <summary>
3736
/// Causes a new untitled file to be created in the editor.

src/PowerShellEditorServices/Services/Extension/EditorOperationsService.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ public async Task SaveFileAsync(string currentPath, string newSavePath)
192192
// from another for the extension API.
193193
public string GetWorkspacePath() => _workspaceService.InitialWorkingDirectory;
194194

195-
public string GetWorkspaceRelativePath(string filePath) => _workspaceService.GetRelativePath(filePath);
195+
public string GetWorkspaceRelativePath(ScriptFile scriptFile) => _workspaceService.GetRelativePath(scriptFile);
196196

197197
public async Task ShowInformationMessageAsync(string message)
198198
{

src/PowerShellEditorServices/Services/PowerShell/Handlers/GetCommentHelpHandler.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ public async Task<CommentHelpRequestResult> Handle(CommentHelpRequestParams requ
5151
{
5252
// check if the previous character is `<` because it invalidates
5353
// the param block the follows it.
54-
IList<string> lines = ScriptFile.GetLinesInternal(funcText);
54+
IList<string> lines = ScriptFile.GetLines(funcText);
5555
int relativeTriggerLine0b = triggerLine - funcExtent.StartLineNumber;
5656
if (relativeTriggerLine0b > 0 && lines[relativeTriggerLine0b].IndexOf("<", StringComparison.OrdinalIgnoreCase) > -1)
5757
{
@@ -68,7 +68,7 @@ public async Task<CommentHelpRequestResult> Handle(CommentHelpRequestParams requ
6868
return result;
6969
}
7070

71-
List<string> helpLines = ScriptFile.GetLinesInternal(helpText);
71+
List<string> helpLines = ScriptFile.GetLines(helpText);
7272

7373
if (helpLocation?.Equals("before", StringComparison.OrdinalIgnoreCase) == false)
7474
{

src/PowerShellEditorServices/Services/TextDocument/ScriptFile.cs

+2-16
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,6 @@ internal sealed class ScriptFile
3131

3232
#region Properties
3333

34-
/// <summary>
35-
/// Gets a unique string that identifies this file. At this time,
36-
/// this property returns a normalized version of the value stored
37-
/// in the FilePath property.
38-
/// </summary>
39-
public string Id => FilePath.ToLower();
40-
4134
/// <summary>
4235
/// Gets the path at which this file resides.
4336
/// </summary>
@@ -173,14 +166,7 @@ internal ScriptFile(
173166
/// </summary>
174167
/// <param name="text">Input string to be split up into lines.</param>
175168
/// <returns>The lines in the string.</returns>
176-
internal static IList<string> GetLines(string text) => GetLinesInternal(text);
177-
178-
/// <summary>
179-
/// Get the lines in a string.
180-
/// </summary>
181-
/// <param name="text">Input string to be split up into lines.</param>
182-
/// <returns>The lines in the string.</returns>
183-
internal static List<string> GetLinesInternal(string text)
169+
internal static List<string> GetLines(string text)
184170
{
185171
if (text == null)
186172
{
@@ -520,7 +506,7 @@ internal void SetFileContents(string fileContents)
520506
{
521507
// Split the file contents into lines and trim
522508
// any carriage returns from the strings.
523-
FileLines = GetLinesInternal(fileContents);
509+
FileLines = GetLines(fileContents);
524510

525511
// Parse the contents to get syntax tree and errors
526512
ParseFileContents();

src/PowerShellEditorServices/Services/Workspace/WorkspaceService.cs

+18-50
Original file line numberDiff line numberDiff line change
@@ -306,27 +306,31 @@ public void CloseFile(ScriptFile scriptFile)
306306
/// <summary>
307307
/// Gets the workspace-relative path of the given file path.
308308
/// </summary>
309-
/// <param name="filePath">The original full file path.</param>
310309
/// <returns>A relative file path</returns>
311-
public string GetRelativePath(string filePath)
310+
public string GetRelativePath(ScriptFile scriptFile)
312311
{
313-
string resolvedPath = filePath;
314-
315-
if (!IsPathInMemory(filePath) && !string.IsNullOrEmpty(InitialWorkingDirectory))
312+
Uri fileUri = scriptFile.DocumentUri.ToUri();
313+
if (!scriptFile.IsInMemory)
316314
{
317-
Uri workspaceUri = new(InitialWorkingDirectory);
318-
Uri fileUri = new(filePath);
319-
320-
resolvedPath = workspaceUri.MakeRelativeUri(fileUri).ToString();
321-
322-
// Convert the directory separators if necessary
323-
if (Path.DirectorySeparatorChar == '\\')
315+
// Support calculating out-of-workspace relative paths in the common case of a
316+
// single workspace folder. Otherwise try to get the matching folder.
317+
foreach (WorkspaceFolder workspaceFolder in WorkspaceFolders)
324318
{
325-
resolvedPath = resolvedPath.Replace('/', '\\');
319+
Uri workspaceUri = workspaceFolder.Uri.ToUri();
320+
if (WorkspaceFolders.Count == 1 || workspaceUri.IsBaseOf(fileUri))
321+
{
322+
return workspaceUri.MakeRelativeUri(fileUri).ToString();
323+
}
326324
}
327325
}
328326

329-
return resolvedPath;
327+
// Default to the absolute file path if possible, otherwise just return the URI. This
328+
// removes the scheme and initial slash when possible.
329+
if (fileUri.IsAbsoluteUri)
330+
{
331+
return fileUri.AbsolutePath;
332+
}
333+
return fileUri.ToString();
330334
}
331335

332336
/// <summary>
@@ -407,42 +411,6 @@ internal static string ReadFileContents(DocumentUri uri)
407411
return reader.ReadToEnd();
408412
}
409413

410-
internal static bool IsPathInMemory(string filePath)
411-
{
412-
bool isInMemory = false;
413-
414-
// In cases where a "virtual" file is displayed in the editor,
415-
// we need to treat the file differently than one that exists
416-
// on disk. A virtual file could be something like a diff
417-
// view of the current file or an untitled file.
418-
try
419-
{
420-
// File system absolute paths will have a URI scheme of file:.
421-
// Other schemes like "untitled:" and "gitlens-git:" will return false for IsFile.
422-
Uri uri = new(filePath);
423-
isInMemory = !uri.IsFile;
424-
}
425-
catch (UriFormatException)
426-
{
427-
// Relative file paths cause a UriFormatException.
428-
// In this case, fallback to using Path.GetFullPath().
429-
try
430-
{
431-
Path.GetFullPath(filePath);
432-
}
433-
catch (Exception ex) when (ex is ArgumentException or NotSupportedException)
434-
{
435-
isInMemory = true;
436-
}
437-
catch (PathTooLongException)
438-
{
439-
// If we ever get here, it should be an actual file so, not in memory
440-
}
441-
}
442-
443-
return isInMemory;
444-
}
445-
446414
internal string ResolveWorkspacePath(string path) => ResolveRelativeScriptPath(InitialWorkingDirectory, path);
447415

448416
internal string ResolveRelativeScriptPath(string baseFilePath, string relativePath)

test/PowerShellEditorServices.Test/Session/WorkspaceTests.cs

+38-45
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
1010
using Microsoft.PowerShell.EditorServices.Test.Shared;
1111
using Microsoft.PowerShell.EditorServices.Services.TextDocument;
1212
using Xunit;
13+
using Microsoft.PowerShell.EditorServices.Utility;
14+
using OmniSharp.Extensions.LanguageServer.Protocol.Models;
15+
using OmniSharp.Extensions.LanguageServer.Protocol;
1316

1417
namespace PowerShellEditorServices.Test.Session
1518
{
@@ -22,27 +25,51 @@ public class WorkspaceTests
2225
? s_lazyDriveLetter.Value
2326
: string.Empty;
2427

28+
internal static ScriptFile CreateScriptFile(string path) => new(path, "", VersionUtils.PSVersion);
29+
30+
2531
[Fact]
2632
public void CanResolveWorkspaceRelativePath()
2733
{
28-
string workspacePath = TestUtilities.NormalizePath("c:/Test/Workspace/");
29-
string testPathInside = TestUtilities.NormalizePath("c:/Test/Workspace/SubFolder/FilePath.ps1");
30-
string testPathOutside = TestUtilities.NormalizePath("c:/Test/PeerPath/FilePath.ps1");
31-
string testPathAnotherDrive = TestUtilities.NormalizePath("z:/TryAndFindMe/FilePath.ps1");
34+
string workspacePath = "c:/Test/Workspace/";
35+
ScriptFile testPathInside = CreateScriptFile("c:/Test/Workspace/SubFolder/FilePath.ps1");
36+
ScriptFile testPathOutside = CreateScriptFile("c:/Test/PeerPath/FilePath.ps1");
37+
ScriptFile testPathAnotherDrive = CreateScriptFile("z:/TryAndFindMe/FilePath.ps1");
3238

3339
WorkspaceService workspace = new(NullLoggerFactory.Instance);
3440

35-
// Test without a workspace path
36-
Assert.Equal(testPathOutside, workspace.GetRelativePath(testPathOutside));
41+
// Test with zero workspace folders
42+
Assert.Equal(
43+
testPathOutside.DocumentUri.ToUri().AbsolutePath,
44+
workspace.GetRelativePath(testPathOutside));
3745

38-
string expectedInsidePath = TestUtilities.NormalizePath("SubFolder/FilePath.ps1");
39-
string expectedOutsidePath = TestUtilities.NormalizePath("../PeerPath/FilePath.ps1");
46+
string expectedInsidePath = "SubFolder/FilePath.ps1";
47+
string expectedOutsidePath = "../PeerPath/FilePath.ps1";
48+
49+
// Test with a single workspace folder
50+
workspace.WorkspaceFolders.Add(new WorkspaceFolder
51+
{
52+
Uri = DocumentUri.FromFileSystemPath(workspacePath)
53+
});
4054

41-
// Test with a workspace path
42-
workspace.InitialWorkingDirectory = workspacePath;
4355
Assert.Equal(expectedInsidePath, workspace.GetRelativePath(testPathInside));
4456
Assert.Equal(expectedOutsidePath, workspace.GetRelativePath(testPathOutside));
45-
Assert.Equal(testPathAnotherDrive, workspace.GetRelativePath(testPathAnotherDrive));
57+
Assert.Equal(
58+
testPathAnotherDrive.DocumentUri.ToUri().AbsolutePath,
59+
workspace.GetRelativePath(testPathAnotherDrive));
60+
61+
// Test with two workspace folders
62+
string anotherWorkspacePath = "c:/Test/AnotherWorkspace/";
63+
ScriptFile anotherTestPathInside = CreateScriptFile("c:/Test/AnotherWorkspace/DifferentFolder/FilePath.ps1");
64+
string anotherExpectedInsidePath = "DifferentFolder/FilePath.ps1";
65+
66+
workspace.WorkspaceFolders.Add(new WorkspaceFolder
67+
{
68+
Uri = DocumentUri.FromFileSystemPath(anotherWorkspacePath)
69+
});
70+
71+
Assert.Equal(expectedInsidePath, workspace.GetRelativePath(testPathInside));
72+
Assert.Equal(anotherExpectedInsidePath, workspace.GetRelativePath(anotherTestPathInside));
4673
}
4774

4875
internal static WorkspaceService FixturesWorkspace()
@@ -143,40 +170,6 @@ public void CanRecurseDirectoryTreeWithGlobs()
143170
}, actual);
144171
}
145172

146-
[Fact]
147-
public void CanDetermineIsPathInMemory()
148-
{
149-
string tempDir = Path.GetTempPath();
150-
string shortDirPath = Path.Combine(tempDir, "GitHub", "PowerShellEditorServices");
151-
string shortFilePath = Path.Combine(shortDirPath, "foo.ps1");
152-
const string shortUriForm = "git:/c%3A/Users/Keith/GitHub/dahlbyk/posh-git/src/PoshGitTypes.ps1?%7B%22path%22%3A%22c%3A%5C%5CUsers%5C%5CKeith%5C%5CGitHub%5C%5Cdahlbyk%5C%5Cposh-git%5C%5Csrc%5C%5CPoshGitTypes.ps1%22%2C%22ref%22%3A%22~%22%7D";
153-
const string longUriForm = "gitlens-git:c%3A%5CUsers%5CKeith%5CGitHub%5Cdahlbyk%5Cposh-git%5Csrc%5CPoshGitTypes%3Ae0022701.ps1?%7B%22fileName%22%3A%22src%2FPoshGitTypes.ps1%22%2C%22repoPath%22%3A%22c%3A%2FUsers%2FKeith%2FGitHub%2Fdahlbyk%2Fposh-git%22%2C%22sha%22%3A%22e0022701fa12e0bc22d0458673d6443c942b974a%22%7D";
154-
155-
string[] inMemoryPaths = new[] {
156-
// Test short non-file paths
157-
"untitled:untitled-1",
158-
shortUriForm,
159-
"inmemory://foo.ps1",
160-
// Test long non-file path
161-
longUriForm
162-
};
163-
164-
Assert.All(inMemoryPaths, (p) => Assert.True(WorkspaceService.IsPathInMemory(p)));
165-
166-
string[] notInMemoryPaths = new[] {
167-
// Test short file absolute paths
168-
shortDirPath,
169-
shortFilePath,
170-
new Uri(shortDirPath).ToString(),
171-
new Uri(shortFilePath).ToString(),
172-
// Test short file relative paths
173-
"foo.ps1",
174-
Path.Combine(new[] { "..", "foo.ps1" })
175-
};
176-
177-
Assert.All(notInMemoryPaths, (p) => Assert.False(WorkspaceService.IsPathInMemory(p)));
178-
}
179-
180173
[Fact]
181174
public void CanOpenAndCloseFile()
182175
{

0 commit comments

Comments
 (0)