diff --git a/src/PowerShellEditorServices/Extensions/Api/WorkspaceService.cs b/src/PowerShellEditorServices/Extensions/Api/WorkspaceService.cs
index 443183f49..8e5472633 100644
--- a/src/PowerShellEditorServices/Extensions/Api/WorkspaceService.cs
+++ b/src/PowerShellEditorServices/Extensions/Api/WorkspaceService.cs
@@ -45,7 +45,7 @@ public interface IEditorScriptFile
public interface IWorkspaceService
{
///
- /// The root path of the workspace.
+ /// The root path of the workspace for the current editor.
///
string WorkspacePath { get; }
@@ -116,7 +116,9 @@ internal WorkspaceService(
ExcludedFileGlobs = _workspaceService.ExcludeFilesGlob.AsReadOnly();
}
- public string WorkspacePath => _workspaceService.WorkspacePath;
+ // TODO: This needs to use the associated EditorContext to get the workspace for the current
+ // editor instead of the initial working directory.
+ public string WorkspacePath => _workspaceService.InitialWorkingDirectory;
public bool FollowSymlinks => _workspaceService.FollowSymlinks;
diff --git a/src/PowerShellEditorServices/Extensions/EditorWorkspace.cs b/src/PowerShellEditorServices/Extensions/EditorWorkspace.cs
index 1a471b034..18f001d56 100644
--- a/src/PowerShellEditorServices/Extensions/EditorWorkspace.cs
+++ b/src/PowerShellEditorServices/Extensions/EditorWorkspace.cs
@@ -18,7 +18,7 @@ public sealed class EditorWorkspace
#region Properties
///
- /// Gets the current workspace path if there is one or null otherwise.
+ /// Gets the current workspace path if there is one for the open editor or null otherwise.
///
public string Path => editorOperations.GetWorkspacePath();
diff --git a/src/PowerShellEditorServices/Server/PsesLanguageServer.cs b/src/PowerShellEditorServices/Server/PsesLanguageServer.cs
index 05099b36b..440e84a70 100644
--- a/src/PowerShellEditorServices/Server/PsesLanguageServer.cs
+++ b/src/PowerShellEditorServices/Server/PsesLanguageServer.cs
@@ -2,6 +2,7 @@
// Licensed under the MIT License.
using System.IO;
+using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
@@ -13,7 +14,6 @@
using Microsoft.PowerShell.EditorServices.Services.Template;
using Newtonsoft.Json.Linq;
using OmniSharp.Extensions.JsonRpc;
-using OmniSharp.Extensions.LanguageServer.Protocol.Models;
using OmniSharp.Extensions.LanguageServer.Protocol.Server;
using OmniSharp.Extensions.LanguageServer.Server;
using Serilog;
@@ -130,12 +130,7 @@ public async Task StartAsync()
WorkspaceService workspaceService = languageServer.Services.GetService();
if (initializeParams.WorkspaceFolders is not null)
{
- // TODO: Support multi-workspace.
- foreach (WorkspaceFolder workspaceFolder in initializeParams.WorkspaceFolders)
- {
- workspaceService.WorkspacePath = workspaceFolder.Uri.GetFileSystemPath();
- break;
- }
+ workspaceService.WorkspaceFolders.AddRange(initializeParams.WorkspaceFolders);
}
// Parse initialization options.
@@ -149,13 +144,19 @@ public async Task StartAsync()
//
// NOTE: The keys start with a lowercase because OmniSharp's client
// (used for testing) forces it to be that way.
- LoadProfiles = initializationOptions?.GetValue("enableProfileLoading")?.Value() ?? true,
- // TODO: Consider deprecating the setting which sets this and
- // instead use WorkspacePath exclusively.
- InitialWorkingDirectory = initializationOptions?.GetValue("initialWorkingDirectory")?.Value() ?? workspaceService.WorkspacePath,
- ShellIntegrationEnabled = initializationOptions?.GetValue("shellIntegrationEnabled")?.Value() ?? false
+ LoadProfiles = initializationOptions?.GetValue("enableProfileLoading")?.Value()
+ ?? true,
+ // First check the setting, then use the first workspace folder,
+ // finally fall back to CWD.
+ InitialWorkingDirectory = initializationOptions?.GetValue("initialWorkingDirectory")?.Value()
+ ?? workspaceService.WorkspaceFolders.FirstOrDefault()?.Uri.GetFileSystemPath()
+ ?? Directory.GetCurrentDirectory(),
+ ShellIntegrationEnabled = initializationOptions?.GetValue("shellIntegrationEnabled")?.Value()
+ ?? false
};
+ workspaceService.InitialWorkingDirectory = hostStartOptions.InitialWorkingDirectory;
+
_psesHost = languageServer.Services.GetService();
return _psesHost.TryStartAsync(hostStartOptions, cancellationToken);
});
diff --git a/src/PowerShellEditorServices/Services/Extension/EditorOperationsService.cs b/src/PowerShellEditorServices/Services/Extension/EditorOperationsService.cs
index 0f0f2a01a..fa6da7f90 100644
--- a/src/PowerShellEditorServices/Services/Extension/EditorOperationsService.cs
+++ b/src/PowerShellEditorServices/Services/Extension/EditorOperationsService.cs
@@ -191,7 +191,9 @@ public async Task SaveFileAsync(string currentPath, string newSavePath)
}).ReturningVoid(CancellationToken.None).ConfigureAwait(false);
}
- public string GetWorkspacePath() => _workspaceService.WorkspacePath;
+ // TODO: This should get the current editor's context and use it to determine which
+ // workspace it's in.
+ public string GetWorkspacePath() => _workspaceService.InitialWorkingDirectory;
public string GetWorkspaceRelativePath(string filePath) => _workspaceService.GetRelativePath(filePath);
diff --git a/src/PowerShellEditorServices/Services/Symbols/SymbolReference.cs b/src/PowerShellEditorServices/Services/Symbols/SymbolReference.cs
index 2c6c6ef8f..5e36bf63b 100644
--- a/src/PowerShellEditorServices/Services/Symbols/SymbolReference.cs
+++ b/src/PowerShellEditorServices/Services/Symbols/SymbolReference.cs
@@ -66,5 +66,19 @@ public SymbolReference(
}
IsDeclaration = isDeclaration;
}
+
+ ///
+ /// This is only used for unit tests!
+ ///
+ internal SymbolReference(string id, SymbolType type)
+ {
+ Id = id;
+ Type = type;
+ Name = "";
+ NameRegion = new("", "", 0, 0, 0, 0, 0, 0);
+ ScriptRegion = NameRegion;
+ SourceLine = "";
+ FilePath = "";
+ }
}
}
diff --git a/src/PowerShellEditorServices/Services/Workspace/Handlers/ConfigurationHandler.cs b/src/PowerShellEditorServices/Services/Workspace/Handlers/ConfigurationHandler.cs
index ea7214a0a..230a82d58 100644
--- a/src/PowerShellEditorServices/Services/Workspace/Handlers/ConfigurationHandler.cs
+++ b/src/PowerShellEditorServices/Services/Workspace/Handlers/ConfigurationHandler.cs
@@ -58,7 +58,7 @@ public override async Task Handle(DidChangeConfigurationParams request, Ca
_configurationService.CurrentSettings.Update(
incomingSettings.Powershell,
- _workspaceService.WorkspacePath,
+ _workspaceService.InitialWorkingDirectory,
_logger);
// Run any events subscribed to configuration updates
diff --git a/src/PowerShellEditorServices/Services/Workspace/WorkspaceService.cs b/src/PowerShellEditorServices/Services/Workspace/WorkspaceService.cs
index 002a757ad..f705101f8 100644
--- a/src/PowerShellEditorServices/Services/Workspace/WorkspaceService.cs
+++ b/src/PowerShellEditorServices/Services/Workspace/WorkspaceService.cs
@@ -1,10 +1,11 @@
-// Copyright (c) Microsoft Corporation.
+// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
+using System.Linq;
using System.Security;
using System.Text;
using Microsoft.Extensions.FileSystemGlobbing;
@@ -13,6 +14,7 @@
using Microsoft.PowerShell.EditorServices.Services.Workspace;
using Microsoft.PowerShell.EditorServices.Utility;
using OmniSharp.Extensions.LanguageServer.Protocol;
+using OmniSharp.Extensions.LanguageServer.Protocol.Models;
namespace Microsoft.PowerShell.EditorServices.Services
{
@@ -58,9 +60,19 @@ internal class WorkspaceService
#region Properties
///
- /// Gets or sets the root path of the workspace.
+ /// Gets or sets the initial working directory.
+ ///
+ /// This is settable by the same key in the initialization options, and likely corresponds
+ /// to the root of the workspace if only one workspace folder is being used. However, in
+ /// multi-root workspaces this may be any workspace folder's root (or none if overridden).
+ ///
///
- public string WorkspacePath { get; set; }
+ public string InitialWorkingDirectory { get; set; }
+
+ ///
+ /// Gets or sets the folders of the workspace.
+ ///
+ public List WorkspaceFolders { get; set; }
///
/// Gets or sets the default list of file globs to exclude during workspace searches.
@@ -83,6 +95,7 @@ public WorkspaceService(ILoggerFactory factory)
{
powerShellVersion = VersionUtils.PSVersion;
logger = factory.CreateLogger();
+ WorkspaceFolders = new List();
ExcludeFilesGlob = new List();
FollowSymlinks = true;
}
@@ -299,9 +312,9 @@ public string GetRelativePath(string filePath)
{
string resolvedPath = filePath;
- if (!IsPathInMemory(filePath) && !string.IsNullOrEmpty(WorkspacePath))
+ if (!IsPathInMemory(filePath) && !string.IsNullOrEmpty(InitialWorkingDirectory))
{
- Uri workspaceUri = new(WorkspacePath);
+ Uri workspaceUri = new(InitialWorkingDirectory);
Uri fileUri = new(filePath);
resolvedPath = workspaceUri.MakeRelativeUri(fileUri).ToString();
@@ -331,39 +344,46 @@ public IEnumerable EnumeratePSFiles()
}
///
- /// Enumerate all the PowerShell (ps1, psm1, psd1) files in the workspace in a recursive manner.
+ /// Enumerate all the PowerShell (ps1, psm1, psd1) files in the workspace folders in a
+ /// recursive manner. Falls back to initial working directory if there are no workspace folders.
///
/// An enumerator over the PowerShell files found in the workspace.
public IEnumerable EnumeratePSFiles(
string[] excludeGlobs,
string[] includeGlobs,
int maxDepth,
- bool ignoreReparsePoints
- )
+ bool ignoreReparsePoints)
{
- if (WorkspacePath is null || !Directory.Exists(WorkspacePath))
- {
- yield break;
- }
+ IEnumerable rootPaths = WorkspaceFolders.Count == 0
+ ? new List { InitialWorkingDirectory }
+ : WorkspaceFolders.Select(i => i.Uri.GetFileSystemPath());
Matcher matcher = new();
foreach (string pattern in includeGlobs) { matcher.AddInclude(pattern); }
foreach (string pattern in excludeGlobs) { matcher.AddExclude(pattern); }
- WorkspaceFileSystemWrapperFactory fsFactory = new(
- WorkspacePath,
- maxDepth,
- VersionUtils.IsNetCore ? s_psFileExtensionsCoreFramework : s_psFileExtensionsFullFramework,
- ignoreReparsePoints,
- logger
- );
- PatternMatchingResult fileMatchResult = matcher.Execute(fsFactory.RootDirectory);
- foreach (FilePatternMatch item in fileMatchResult.Files)
+ foreach (string rootPath in rootPaths)
{
- // item.Path always contains forward slashes in paths when it should be backslashes on Windows.
- // Since we're returning strings here, it's important to use the correct directory separator.
- string path = VersionUtils.IsWindows ? item.Path.Replace('/', Path.DirectorySeparatorChar) : item.Path;
- yield return Path.Combine(WorkspacePath, path);
+ if (!Directory.Exists(rootPath))
+ {
+ continue;
+ }
+
+ WorkspaceFileSystemWrapperFactory fsFactory = new(
+ rootPath,
+ maxDepth,
+ VersionUtils.IsNetCore ? s_psFileExtensionsCoreFramework : s_psFileExtensionsFullFramework,
+ ignoreReparsePoints,
+ logger);
+
+ PatternMatchingResult fileMatchResult = matcher.Execute(fsFactory.RootDirectory);
+ foreach (FilePatternMatch item in fileMatchResult.Files)
+ {
+ // item.Path always contains forward slashes in paths when it should be backslashes on Windows.
+ // Since we're returning strings here, it's important to use the correct directory separator.
+ string path = VersionUtils.IsWindows ? item.Path.Replace('/', Path.DirectorySeparatorChar) : item.Path;
+ yield return Path.Combine(rootPath, path);
+ }
}
}
@@ -423,7 +443,7 @@ internal static bool IsPathInMemory(string filePath)
return isInMemory;
}
- internal string ResolveWorkspacePath(string path) => ResolveRelativeScriptPath(WorkspacePath, path);
+ internal string ResolveWorkspacePath(string path) => ResolveRelativeScriptPath(InitialWorkingDirectory, path);
internal string ResolveRelativeScriptPath(string baseFilePath, string relativePath)
{
diff --git a/test/PowerShellEditorServices.Test/Language/SymbolsServiceTests.cs b/test/PowerShellEditorServices.Test/Language/SymbolsServiceTests.cs
index 257e36880..c633c6326 100644
--- a/test/PowerShellEditorServices.Test/Language/SymbolsServiceTests.cs
+++ b/test/PowerShellEditorServices.Test/Language/SymbolsServiceTests.cs
@@ -23,6 +23,8 @@
using Microsoft.PowerShell.EditorServices.Test.Shared.SymbolDetails;
using Microsoft.PowerShell.EditorServices.Test.Shared.Symbols;
using Microsoft.PowerShell.EditorServices.Utility;
+using OmniSharp.Extensions.LanguageServer.Protocol;
+using OmniSharp.Extensions.LanguageServer.Protocol.Models;
using Xunit;
namespace PowerShellEditorServices.Test.Language
@@ -38,10 +40,11 @@ public class SymbolsServiceTests : IDisposable
public SymbolsServiceTests()
{
psesHost = PsesHostFactory.Create(NullLoggerFactory.Instance);
- workspace = new WorkspaceService(NullLoggerFactory.Instance)
+ workspace = new WorkspaceService(NullLoggerFactory.Instance);
+ workspace.WorkspaceFolders.Add(new WorkspaceFolder
{
- WorkspacePath = TestUtilities.GetSharedPath("References")
- };
+ Uri = DocumentUri.FromFileSystemPath(TestUtilities.GetSharedPath("References"))
+ });
symbolsService = new SymbolsService(
NullLoggerFactory.Instance,
psesHost,
@@ -226,6 +229,23 @@ public async Task FindsReferencesOnFunction()
});
}
+ [Fact]
+ public async Task FindsReferenceAcrossMultiRootWorkspace()
+ {
+ workspace.WorkspaceFolders = new[] { "Debugging", "ParameterHints", "SymbolDetails" }
+ .Select(i => new WorkspaceFolder
+ {
+ Uri = DocumentUri.FromFileSystemPath(TestUtilities.GetSharedPath(i))
+ }).ToList();
+
+ SymbolReference symbol = new("fn Get-Process", SymbolType.Function);
+ IEnumerable symbols = await symbolsService.ScanForReferencesOfSymbolAsync(symbol).ConfigureAwait(true);
+ Assert.Collection(symbols.OrderBy(i => i.FilePath),
+ i => Assert.EndsWith("VariableTest.ps1", i.FilePath),
+ i => Assert.EndsWith("ParamHints.ps1", i.FilePath),
+ i => Assert.EndsWith("SymbolDetails.ps1", i.FilePath));
+ }
+
[Fact]
public async Task FindsReferencesOnFunctionIncludingAliases()
{
diff --git a/test/PowerShellEditorServices.Test/Session/WorkspaceTests.cs b/test/PowerShellEditorServices.Test/Session/WorkspaceTests.cs
index c76da5328..2c7a44279 100644
--- a/test/PowerShellEditorServices.Test/Session/WorkspaceTests.cs
+++ b/test/PowerShellEditorServices.Test/Session/WorkspaceTests.cs
@@ -39,7 +39,7 @@ public void CanResolveWorkspaceRelativePath()
string expectedOutsidePath = TestUtilities.NormalizePath("../PeerPath/FilePath.ps1");
// Test with a workspace path
- workspace.WorkspacePath = workspacePath;
+ workspace.InitialWorkingDirectory = workspacePath;
Assert.Equal(expectedInsidePath, workspace.GetRelativePath(testPathInside));
Assert.Equal(expectedOutsidePath, workspace.GetRelativePath(testPathOutside));
Assert.Equal(testPathAnotherDrive, workspace.GetRelativePath(testPathAnotherDrive));
@@ -49,7 +49,7 @@ internal static WorkspaceService FixturesWorkspace()
{
return new WorkspaceService(NullLoggerFactory.Instance)
{
- WorkspacePath = TestUtilities.NormalizePath("Fixtures/Workspace")
+ InitialWorkingDirectory = TestUtilities.NormalizePath("Fixtures/Workspace")
};
}
@@ -94,10 +94,10 @@ public void CanRecurseDirectoryTree()
List expected = new()
{
- Path.Combine(workspace.WorkspacePath, "nested", "donotfind.ps1"),
- Path.Combine(workspace.WorkspacePath, "nested", "nestedmodule.psd1"),
- Path.Combine(workspace.WorkspacePath, "nested", "nestedmodule.psm1"),
- Path.Combine(workspace.WorkspacePath, "rootfile.ps1")
+ Path.Combine(workspace.InitialWorkingDirectory, "nested", "donotfind.ps1"),
+ Path.Combine(workspace.InitialWorkingDirectory, "nested", "nestedmodule.psd1"),
+ Path.Combine(workspace.InitialWorkingDirectory, "nested", "nestedmodule.psm1"),
+ Path.Combine(workspace.InitialWorkingDirectory, "rootfile.ps1")
};
// .NET Core doesn't appear to use the same three letter pattern matching rule although the docs
@@ -105,7 +105,7 @@ public void CanRecurseDirectoryTree()
// 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_
if (RuntimeInformation.FrameworkDescription.StartsWith(".NET Framework"))
{
- expected.Insert(3, Path.Combine(workspace.WorkspacePath, "other", "other.ps1xml"));
+ expected.Insert(3, Path.Combine(workspace.InitialWorkingDirectory, "other", "other.ps1xml"));
}
Assert.Equal(expected, actual);
@@ -122,7 +122,7 @@ public void CanRecurseDirectoryTreeWithLimit()
maxDepth: 1,
ignoreReparsePoints: s_defaultIgnoreReparsePoints
);
- Assert.Equal(new[] { Path.Combine(workspace.WorkspacePath, "rootfile.ps1") }, actual);
+ Assert.Equal(new[] { Path.Combine(workspace.InitialWorkingDirectory, "rootfile.ps1") }, actual);
}
[Fact]
@@ -138,8 +138,8 @@ public void CanRecurseDirectoryTreeWithGlobs()
);
Assert.Equal(new[] {
- Path.Combine(workspace.WorkspacePath, "nested", "nestedmodule.psd1"),
- Path.Combine(workspace.WorkspacePath, "rootfile.ps1")
+ Path.Combine(workspace.InitialWorkingDirectory, "nested", "nestedmodule.psd1"),
+ Path.Combine(workspace.InitialWorkingDirectory, "rootfile.ps1")
}, actual);
}
@@ -181,7 +181,7 @@ public void CanDetermineIsPathInMemory()
public void CanOpenAndCloseFile()
{
WorkspaceService workspace = FixturesWorkspace();
- string filePath = Path.GetFullPath(Path.Combine(workspace.WorkspacePath, "rootfile.ps1"));
+ string filePath = Path.GetFullPath(Path.Combine(workspace.InitialWorkingDirectory, "rootfile.ps1"));
ScriptFile file = workspace.GetFile(filePath);
Assert.Equal(workspace.GetOpenedFiles(), new[] { file });