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
Show file tree
Hide file tree
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
Expand Up @@ -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; }

Expand Down Expand Up @@ -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;

Expand Down
2 changes: 1 addition & 1 deletion src/PowerShellEditorServices/Extensions/EditorWorkspace.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down
25 changes: 13 additions & 12 deletions src/PowerShellEditorServices/Server/PsesLanguageServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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.
Expand All @@ -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);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
14 changes: 14 additions & 0 deletions src/PowerShellEditorServices/Services/Symbols/SymbolReference.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Up @@ -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
Expand Down
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;
Expand All @@ -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
{
Expand Down Expand Up @@ -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.
Expand All @@ -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;
}
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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);
}
}
}

Expand Down Expand Up @@ -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)
{
Expand Down
26 changes: 23 additions & 3 deletions test/PowerShellEditorServices.Test/Language/SymbolsServiceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -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()
{
Expand Down
22 changes: 11 additions & 11 deletions test/PowerShellEditorServices.Test/Session/WorkspaceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand All @@ -49,7 +49,7 @@ internal static WorkspaceService FixturesWorkspace()
{
return new WorkspaceService(NullLoggerFactory.Instance)
{
WorkspacePath = TestUtilities.NormalizePath("Fixtures/Workspace")
InitialWorkingDirectory = TestUtilities.NormalizePath("Fixtures/Workspace")
};
}

Expand Down Expand Up @@ -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);
Expand All @@ -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]
Expand All @@ -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);
}

Expand Down Expand Up @@ -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 });
Expand Down