Skip to content

Commit 9aa5059

Browse files
Add WorkspaceFolders and use it when enumerating files (#1995)
* Rename `WorkspacePath` to `InitialWorkingDirectory` And note where the API needs to be updated to get the workspace path for the current open editor (since there could be multiple workspaces). * Add `WorkspaceFolders` and use it when enumerating files * Add symbols across multi-root workspace unit test
1 parent 4e651e0 commit 9aa5059

File tree

9 files changed

+116
-57
lines changed

9 files changed

+116
-57
lines changed

src/PowerShellEditorServices/Extensions/Api/WorkspaceService.cs

+4-2
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ public interface IEditorScriptFile
4545
public interface IWorkspaceService
4646
{
4747
/// <summary>
48-
/// The root path of the workspace.
48+
/// The root path of the workspace for the current editor.
4949
/// </summary>
5050
string WorkspacePath { get; }
5151

@@ -116,7 +116,9 @@ internal WorkspaceService(
116116
ExcludedFileGlobs = _workspaceService.ExcludeFilesGlob.AsReadOnly();
117117
}
118118

119-
public string WorkspacePath => _workspaceService.WorkspacePath;
119+
// TODO: This needs to use the associated EditorContext to get the workspace for the current
120+
// editor instead of the initial working directory.
121+
public string WorkspacePath => _workspaceService.InitialWorkingDirectory;
120122

121123
public bool FollowSymlinks => _workspaceService.FollowSymlinks;
122124

src/PowerShellEditorServices/Extensions/EditorWorkspace.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ public sealed class EditorWorkspace
1818
#region Properties
1919

2020
/// <summary>
21-
/// Gets the current workspace path if there is one or null otherwise.
21+
/// Gets the current workspace path if there is one for the open editor or null otherwise.
2222
/// </summary>
2323
public string Path => editorOperations.GetWorkspacePath();
2424

src/PowerShellEditorServices/Server/PsesLanguageServer.cs

+13-12
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Licensed under the MIT License.
33

44
using System.IO;
5+
using System.Linq;
56
using System.Threading.Tasks;
67
using Microsoft.Extensions.DependencyInjection;
78
using Microsoft.Extensions.Logging;
@@ -13,7 +14,6 @@
1314
using Microsoft.PowerShell.EditorServices.Services.Template;
1415
using Newtonsoft.Json.Linq;
1516
using OmniSharp.Extensions.JsonRpc;
16-
using OmniSharp.Extensions.LanguageServer.Protocol.Models;
1717
using OmniSharp.Extensions.LanguageServer.Protocol.Server;
1818
using OmniSharp.Extensions.LanguageServer.Server;
1919
using Serilog;
@@ -130,12 +130,7 @@ public async Task StartAsync()
130130
WorkspaceService workspaceService = languageServer.Services.GetService<WorkspaceService>();
131131
if (initializeParams.WorkspaceFolders is not null)
132132
{
133-
// TODO: Support multi-workspace.
134-
foreach (WorkspaceFolder workspaceFolder in initializeParams.WorkspaceFolders)
135-
{
136-
workspaceService.WorkspacePath = workspaceFolder.Uri.GetFileSystemPath();
137-
break;
138-
}
133+
workspaceService.WorkspaceFolders.AddRange(initializeParams.WorkspaceFolders);
139134
}
140135

141136
// Parse initialization options.
@@ -149,13 +144,19 @@ public async Task StartAsync()
149144
//
150145
// NOTE: The keys start with a lowercase because OmniSharp's client
151146
// (used for testing) forces it to be that way.
152-
LoadProfiles = initializationOptions?.GetValue("enableProfileLoading")?.Value<bool>() ?? true,
153-
// TODO: Consider deprecating the setting which sets this and
154-
// instead use WorkspacePath exclusively.
155-
InitialWorkingDirectory = initializationOptions?.GetValue("initialWorkingDirectory")?.Value<string>() ?? workspaceService.WorkspacePath,
156-
ShellIntegrationEnabled = initializationOptions?.GetValue("shellIntegrationEnabled")?.Value<bool>() ?? false
147+
LoadProfiles = initializationOptions?.GetValue("enableProfileLoading")?.Value<bool>()
148+
?? true,
149+
// First check the setting, then use the first workspace folder,
150+
// finally fall back to CWD.
151+
InitialWorkingDirectory = initializationOptions?.GetValue("initialWorkingDirectory")?.Value<string>()
152+
?? workspaceService.WorkspaceFolders.FirstOrDefault()?.Uri.GetFileSystemPath()
153+
?? Directory.GetCurrentDirectory(),
154+
ShellIntegrationEnabled = initializationOptions?.GetValue("shellIntegrationEnabled")?.Value<bool>()
155+
?? false
157156
};
158157

158+
workspaceService.InitialWorkingDirectory = hostStartOptions.InitialWorkingDirectory;
159+
159160
_psesHost = languageServer.Services.GetService<PsesInternalHost>();
160161
return _psesHost.TryStartAsync(hostStartOptions, cancellationToken);
161162
});

src/PowerShellEditorServices/Services/Extension/EditorOperationsService.cs

+3-1
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,9 @@ public async Task SaveFileAsync(string currentPath, string newSavePath)
191191
}).ReturningVoid(CancellationToken.None).ConfigureAwait(false);
192192
}
193193

194-
public string GetWorkspacePath() => _workspaceService.WorkspacePath;
194+
// TODO: This should get the current editor's context and use it to determine which
195+
// workspace it's in.
196+
public string GetWorkspacePath() => _workspaceService.InitialWorkingDirectory;
195197

196198
public string GetWorkspaceRelativePath(string filePath) => _workspaceService.GetRelativePath(filePath);
197199

src/PowerShellEditorServices/Services/Symbols/SymbolReference.cs

+14
Original file line numberDiff line numberDiff line change
@@ -66,5 +66,19 @@ public SymbolReference(
6666
}
6767
IsDeclaration = isDeclaration;
6868
}
69+
70+
/// <summary>
71+
/// This is only used for unit tests!
72+
/// </summary>
73+
internal SymbolReference(string id, SymbolType type)
74+
{
75+
Id = id;
76+
Type = type;
77+
Name = "";
78+
NameRegion = new("", "", 0, 0, 0, 0, 0, 0);
79+
ScriptRegion = NameRegion;
80+
SourceLine = "";
81+
FilePath = "";
82+
}
6983
}
7084
}

src/PowerShellEditorServices/Services/Workspace/Handlers/ConfigurationHandler.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ public override async Task<Unit> Handle(DidChangeConfigurationParams request, Ca
5858

5959
_configurationService.CurrentSettings.Update(
6060
incomingSettings.Powershell,
61-
_workspaceService.WorkspacePath,
61+
_workspaceService.InitialWorkingDirectory,
6262
_logger);
6363

6464
// Run any events subscribed to configuration updates

src/PowerShellEditorServices/Services/Workspace/WorkspaceService.cs

+46-26
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
// Copyright (c) Microsoft Corporation.
1+
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT License.
33

44
using System;
55
using System.Collections.Concurrent;
66
using System.Collections.Generic;
77
using System.IO;
8+
using System.Linq;
89
using System.Security;
910
using System.Text;
1011
using Microsoft.Extensions.FileSystemGlobbing;
@@ -13,6 +14,7 @@
1314
using Microsoft.PowerShell.EditorServices.Services.Workspace;
1415
using Microsoft.PowerShell.EditorServices.Utility;
1516
using OmniSharp.Extensions.LanguageServer.Protocol;
17+
using OmniSharp.Extensions.LanguageServer.Protocol.Models;
1618

1719
namespace Microsoft.PowerShell.EditorServices.Services
1820
{
@@ -58,9 +60,19 @@ internal class WorkspaceService
5860
#region Properties
5961

6062
/// <summary>
61-
/// Gets or sets the root path of the workspace.
63+
/// <para>Gets or sets the initial working directory.</para>
64+
/// <para>
65+
/// This is settable by the same key in the initialization options, and likely corresponds
66+
/// to the root of the workspace if only one workspace folder is being used. However, in
67+
/// multi-root workspaces this may be any workspace folder's root (or none if overridden).
68+
/// </para>
6269
/// </summary>
63-
public string WorkspacePath { get; set; }
70+
public string InitialWorkingDirectory { get; set; }
71+
72+
/// <summary>
73+
/// Gets or sets the folders of the workspace.
74+
/// </summary>
75+
public List<WorkspaceFolder> WorkspaceFolders { get; set; }
6476

6577
/// <summary>
6678
/// Gets or sets the default list of file globs to exclude during workspace searches.
@@ -83,6 +95,7 @@ public WorkspaceService(ILoggerFactory factory)
8395
{
8496
powerShellVersion = VersionUtils.PSVersion;
8597
logger = factory.CreateLogger<WorkspaceService>();
98+
WorkspaceFolders = new List<WorkspaceFolder>();
8699
ExcludeFilesGlob = new List<string>();
87100
FollowSymlinks = true;
88101
}
@@ -299,9 +312,9 @@ public string GetRelativePath(string filePath)
299312
{
300313
string resolvedPath = filePath;
301314

302-
if (!IsPathInMemory(filePath) && !string.IsNullOrEmpty(WorkspacePath))
315+
if (!IsPathInMemory(filePath) && !string.IsNullOrEmpty(InitialWorkingDirectory))
303316
{
304-
Uri workspaceUri = new(WorkspacePath);
317+
Uri workspaceUri = new(InitialWorkingDirectory);
305318
Uri fileUri = new(filePath);
306319

307320
resolvedPath = workspaceUri.MakeRelativeUri(fileUri).ToString();
@@ -331,39 +344,46 @@ public IEnumerable<string> EnumeratePSFiles()
331344
}
332345

333346
/// <summary>
334-
/// Enumerate all the PowerShell (ps1, psm1, psd1) files in the workspace in a recursive manner.
347+
/// Enumerate all the PowerShell (ps1, psm1, psd1) files in the workspace folders in a
348+
/// recursive manner. Falls back to initial working directory if there are no workspace folders.
335349
/// </summary>
336350
/// <returns>An enumerator over the PowerShell files found in the workspace.</returns>
337351
public IEnumerable<string> EnumeratePSFiles(
338352
string[] excludeGlobs,
339353
string[] includeGlobs,
340354
int maxDepth,
341-
bool ignoreReparsePoints
342-
)
355+
bool ignoreReparsePoints)
343356
{
344-
if (WorkspacePath is null || !Directory.Exists(WorkspacePath))
345-
{
346-
yield break;
347-
}
357+
IEnumerable<string> rootPaths = WorkspaceFolders.Count == 0
358+
? new List<string> { InitialWorkingDirectory }
359+
: WorkspaceFolders.Select(i => i.Uri.GetFileSystemPath());
348360

349361
Matcher matcher = new();
350362
foreach (string pattern in includeGlobs) { matcher.AddInclude(pattern); }
351363
foreach (string pattern in excludeGlobs) { matcher.AddExclude(pattern); }
352364

353-
WorkspaceFileSystemWrapperFactory fsFactory = new(
354-
WorkspacePath,
355-
maxDepth,
356-
VersionUtils.IsNetCore ? s_psFileExtensionsCoreFramework : s_psFileExtensionsFullFramework,
357-
ignoreReparsePoints,
358-
logger
359-
);
360-
PatternMatchingResult fileMatchResult = matcher.Execute(fsFactory.RootDirectory);
361-
foreach (FilePatternMatch item in fileMatchResult.Files)
365+
foreach (string rootPath in rootPaths)
362366
{
363-
// item.Path always contains forward slashes in paths when it should be backslashes on Windows.
364-
// Since we're returning strings here, it's important to use the correct directory separator.
365-
string path = VersionUtils.IsWindows ? item.Path.Replace('/', Path.DirectorySeparatorChar) : item.Path;
366-
yield return Path.Combine(WorkspacePath, path);
367+
if (!Directory.Exists(rootPath))
368+
{
369+
continue;
370+
}
371+
372+
WorkspaceFileSystemWrapperFactory fsFactory = new(
373+
rootPath,
374+
maxDepth,
375+
VersionUtils.IsNetCore ? s_psFileExtensionsCoreFramework : s_psFileExtensionsFullFramework,
376+
ignoreReparsePoints,
377+
logger);
378+
379+
PatternMatchingResult fileMatchResult = matcher.Execute(fsFactory.RootDirectory);
380+
foreach (FilePatternMatch item in fileMatchResult.Files)
381+
{
382+
// item.Path always contains forward slashes in paths when it should be backslashes on Windows.
383+
// Since we're returning strings here, it's important to use the correct directory separator.
384+
string path = VersionUtils.IsWindows ? item.Path.Replace('/', Path.DirectorySeparatorChar) : item.Path;
385+
yield return Path.Combine(rootPath, path);
386+
}
367387
}
368388
}
369389

@@ -423,7 +443,7 @@ internal static bool IsPathInMemory(string filePath)
423443
return isInMemory;
424444
}
425445

426-
internal string ResolveWorkspacePath(string path) => ResolveRelativeScriptPath(WorkspacePath, path);
446+
internal string ResolveWorkspacePath(string path) => ResolveRelativeScriptPath(InitialWorkingDirectory, path);
427447

428448
internal string ResolveRelativeScriptPath(string baseFilePath, string relativePath)
429449
{

test/PowerShellEditorServices.Test/Language/SymbolsServiceTests.cs

+23-3
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
using Microsoft.PowerShell.EditorServices.Test.Shared.SymbolDetails;
2424
using Microsoft.PowerShell.EditorServices.Test.Shared.Symbols;
2525
using Microsoft.PowerShell.EditorServices.Utility;
26+
using OmniSharp.Extensions.LanguageServer.Protocol;
27+
using OmniSharp.Extensions.LanguageServer.Protocol.Models;
2628
using Xunit;
2729

2830
namespace PowerShellEditorServices.Test.Language
@@ -38,10 +40,11 @@ public class SymbolsServiceTests : IDisposable
3840
public SymbolsServiceTests()
3941
{
4042
psesHost = PsesHostFactory.Create(NullLoggerFactory.Instance);
41-
workspace = new WorkspaceService(NullLoggerFactory.Instance)
43+
workspace = new WorkspaceService(NullLoggerFactory.Instance);
44+
workspace.WorkspaceFolders.Add(new WorkspaceFolder
4245
{
43-
WorkspacePath = TestUtilities.GetSharedPath("References")
44-
};
46+
Uri = DocumentUri.FromFileSystemPath(TestUtilities.GetSharedPath("References"))
47+
});
4548
symbolsService = new SymbolsService(
4649
NullLoggerFactory.Instance,
4750
psesHost,
@@ -233,6 +236,23 @@ public async Task FindsReferencesOnFunction()
233236
});
234237
}
235238

239+
[Fact]
240+
public async Task FindsReferenceAcrossMultiRootWorkspace()
241+
{
242+
workspace.WorkspaceFolders = new[] { "Debugging", "ParameterHints", "SymbolDetails" }
243+
.Select(i => new WorkspaceFolder
244+
{
245+
Uri = DocumentUri.FromFileSystemPath(TestUtilities.GetSharedPath(i))
246+
}).ToList();
247+
248+
SymbolReference symbol = new("fn Get-Process", SymbolType.Function);
249+
IEnumerable<SymbolReference> symbols = await symbolsService.ScanForReferencesOfSymbolAsync(symbol).ConfigureAwait(true);
250+
Assert.Collection(symbols.OrderBy(i => i.FilePath),
251+
i => Assert.EndsWith("VariableTest.ps1", i.FilePath),
252+
i => Assert.EndsWith("ParamHints.ps1", i.FilePath),
253+
i => Assert.EndsWith("SymbolDetails.ps1", i.FilePath));
254+
}
255+
236256
[Fact]
237257
public async Task FindsReferencesOnFunctionIncludingAliases()
238258
{

test/PowerShellEditorServices.Test/Session/WorkspaceTests.cs

+11-11
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ public void CanResolveWorkspaceRelativePath()
3939
string expectedOutsidePath = TestUtilities.NormalizePath("../PeerPath/FilePath.ps1");
4040

4141
// Test with a workspace path
42-
workspace.WorkspacePath = workspacePath;
42+
workspace.InitialWorkingDirectory = workspacePath;
4343
Assert.Equal(expectedInsidePath, workspace.GetRelativePath(testPathInside));
4444
Assert.Equal(expectedOutsidePath, workspace.GetRelativePath(testPathOutside));
4545
Assert.Equal(testPathAnotherDrive, workspace.GetRelativePath(testPathAnotherDrive));
@@ -49,7 +49,7 @@ internal static WorkspaceService FixturesWorkspace()
4949
{
5050
return new WorkspaceService(NullLoggerFactory.Instance)
5151
{
52-
WorkspacePath = TestUtilities.NormalizePath("Fixtures/Workspace")
52+
InitialWorkingDirectory = TestUtilities.NormalizePath("Fixtures/Workspace")
5353
};
5454
}
5555

@@ -94,18 +94,18 @@ public void CanRecurseDirectoryTree()
9494

9595
List<string> expected = new()
9696
{
97-
Path.Combine(workspace.WorkspacePath, "nested", "donotfind.ps1"),
98-
Path.Combine(workspace.WorkspacePath, "nested", "nestedmodule.psd1"),
99-
Path.Combine(workspace.WorkspacePath, "nested", "nestedmodule.psm1"),
100-
Path.Combine(workspace.WorkspacePath, "rootfile.ps1")
97+
Path.Combine(workspace.InitialWorkingDirectory, "nested", "donotfind.ps1"),
98+
Path.Combine(workspace.InitialWorkingDirectory, "nested", "nestedmodule.psd1"),
99+
Path.Combine(workspace.InitialWorkingDirectory, "nested", "nestedmodule.psm1"),
100+
Path.Combine(workspace.InitialWorkingDirectory, "rootfile.ps1")
101101
};
102102

103103
// .NET Core doesn't appear to use the same three letter pattern matching rule although the docs
104104
// suggest it should be find the '.ps1xml' files because we search for the pattern '*.ps1'
105105
// ref https://docs.microsoft.com/en-us/dotnet/api/system.io.directory.getfiles?view=netcore-2.1#System_IO_Directory_GetFiles_System_String_System_String_System_IO_EnumerationOptions_
106106
if (RuntimeInformation.FrameworkDescription.StartsWith(".NET Framework"))
107107
{
108-
expected.Insert(3, Path.Combine(workspace.WorkspacePath, "other", "other.ps1xml"));
108+
expected.Insert(3, Path.Combine(workspace.InitialWorkingDirectory, "other", "other.ps1xml"));
109109
}
110110

111111
Assert.Equal(expected, actual);
@@ -122,7 +122,7 @@ public void CanRecurseDirectoryTreeWithLimit()
122122
maxDepth: 1,
123123
ignoreReparsePoints: s_defaultIgnoreReparsePoints
124124
);
125-
Assert.Equal(new[] { Path.Combine(workspace.WorkspacePath, "rootfile.ps1") }, actual);
125+
Assert.Equal(new[] { Path.Combine(workspace.InitialWorkingDirectory, "rootfile.ps1") }, actual);
126126
}
127127

128128
[Fact]
@@ -138,8 +138,8 @@ public void CanRecurseDirectoryTreeWithGlobs()
138138
);
139139

140140
Assert.Equal(new[] {
141-
Path.Combine(workspace.WorkspacePath, "nested", "nestedmodule.psd1"),
142-
Path.Combine(workspace.WorkspacePath, "rootfile.ps1")
141+
Path.Combine(workspace.InitialWorkingDirectory, "nested", "nestedmodule.psd1"),
142+
Path.Combine(workspace.InitialWorkingDirectory, "rootfile.ps1")
143143
}, actual);
144144
}
145145

@@ -181,7 +181,7 @@ public void CanDetermineIsPathInMemory()
181181
public void CanOpenAndCloseFile()
182182
{
183183
WorkspaceService workspace = FixturesWorkspace();
184-
string filePath = Path.GetFullPath(Path.Combine(workspace.WorkspacePath, "rootfile.ps1"));
184+
string filePath = Path.GetFullPath(Path.Combine(workspace.InitialWorkingDirectory, "rootfile.ps1"));
185185

186186
ScriptFile file = workspace.GetFile(filePath);
187187
Assert.Equal(workspace.GetOpenedFiles(), new[] { file });

0 commit comments

Comments
 (0)