Skip to content

Add WorkspaceFolders and use it when enumerating files #1995

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Feb 13, 2023
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -45,7 +45,7 @@ public interface IEditorScriptFile
public interface IWorkspaceService
{
/// <summary>
/// The root path of the workspace.
/// The root path of the workspace for the current editor.
/// </summary>
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;

2 changes: 1 addition & 1 deletion src/PowerShellEditorServices/Extensions/EditorWorkspace.cs
Original file line number Diff line number Diff line change
@@ -18,7 +18,7 @@ public sealed class EditorWorkspace
#region Properties

/// <summary>
/// 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.
/// </summary>
public string Path => editorOperations.GetWorkspacePath();

25 changes: 13 additions & 12 deletions src/PowerShellEditorServices/Server/PsesLanguageServer.cs
Original file line number Diff line number Diff line change
@@ -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<WorkspaceService>();
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<bool>() ?? true,
// TODO: Consider deprecating the setting which sets this and
// instead use WorkspacePath exclusively.
InitialWorkingDirectory = initializationOptions?.GetValue("initialWorkingDirectory")?.Value<string>() ?? workspaceService.WorkspacePath,
ShellIntegrationEnabled = initializationOptions?.GetValue("shellIntegrationEnabled")?.Value<bool>() ?? false
LoadProfiles = initializationOptions?.GetValue("enableProfileLoading")?.Value<bool>()
?? true,
// First check the setting, then use the first workspace folder,
// finally fall back to CWD.
InitialWorkingDirectory = initializationOptions?.GetValue("initialWorkingDirectory")?.Value<string>()
?? workspaceService.WorkspaceFolders.FirstOrDefault()?.Uri.GetFileSystemPath()
?? Directory.GetCurrentDirectory(),
ShellIntegrationEnabled = initializationOptions?.GetValue("shellIntegrationEnabled")?.Value<bool>()
?? false
};

workspaceService.InitialWorkingDirectory = hostStartOptions.InitialWorkingDirectory;

_psesHost = languageServer.Services.GetService<PsesInternalHost>();
return _psesHost.TryStartAsync(hostStartOptions, cancellationToken);
});
Original file line number Diff line number Diff line change
@@ -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);

14 changes: 14 additions & 0 deletions src/PowerShellEditorServices/Services/Symbols/SymbolReference.cs
Original file line number Diff line number Diff line change
@@ -66,5 +66,19 @@ public SymbolReference(
}
IsDeclaration = isDeclaration;
}

/// <summary>
/// This is only used for unit tests!
/// </summary>
internal SymbolReference(string id, SymbolType type)
{
Id = id;
Type = type;
Name = "";
NameRegion = new("", "", 0, 0, 0, 0, 0, 0);
ScriptRegion = NameRegion;
SourceLine = "";
FilePath = "";
}
}
}
Original file line number Diff line number Diff line change
@@ -58,7 +58,7 @@ public override async Task<Unit> Handle(DidChangeConfigurationParams request, Ca

_configurationService.CurrentSettings.Update(
incomingSettings.Powershell,
_workspaceService.WorkspacePath,
_workspaceService.InitialWorkingDirectory,
_logger);

// Run any events subscribed to configuration updates
72 changes: 46 additions & 26 deletions src/PowerShellEditorServices/Services/Workspace/WorkspaceService.cs
Original file line number Diff line number Diff line change
@@ -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

/// <summary>
/// Gets or sets the root path of the workspace.
/// <para>Gets or sets the initial working directory.</para>
/// <para>
/// 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).
/// </para>
/// </summary>
public string WorkspacePath { get; set; }
public string InitialWorkingDirectory { get; set; }

/// <summary>
/// Gets or sets the folders of the workspace.
/// </summary>
public List<WorkspaceFolder> WorkspaceFolders { get; set; }

/// <summary>
/// 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<WorkspaceService>();
WorkspaceFolders = new List<WorkspaceFolder>();
ExcludeFilesGlob = new List<string>();
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<string> EnumeratePSFiles()
}

/// <summary>
/// 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.
/// </summary>
/// <returns>An enumerator over the PowerShell files found in the workspace.</returns>
public IEnumerable<string> EnumeratePSFiles(
string[] excludeGlobs,
string[] includeGlobs,
int maxDepth,
bool ignoreReparsePoints
)
bool ignoreReparsePoints)
{
if (WorkspacePath is null || !Directory.Exists(WorkspacePath))
{
yield break;
}
IEnumerable<string> rootPaths = WorkspaceFolders.Count == 0
? new List<string> { 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)
{
26 changes: 23 additions & 3 deletions test/PowerShellEditorServices.Test/Language/SymbolsServiceTests.cs
Original file line number Diff line number Diff line change
@@ -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<SymbolReference> 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()
{
22 changes: 11 additions & 11 deletions test/PowerShellEditorServices.Test/Session/WorkspaceTests.cs
Original file line number Diff line number Diff line change
@@ -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,18 +94,18 @@ public void CanRecurseDirectoryTree()

List<string> 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
// suggest it should be find the '.ps1xml' files because we search for the pattern '*.ps1'
// 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 });