diff --git a/src/PowerShellEditorServices/Server/PsesLanguageServer.cs b/src/PowerShellEditorServices/Server/PsesLanguageServer.cs index 042e4e8fa..8d28618c7 100644 --- a/src/PowerShellEditorServices/Server/PsesLanguageServer.cs +++ b/src/PowerShellEditorServices/Server/PsesLanguageServer.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; @@ -16,6 +17,7 @@ using Newtonsoft.Json.Linq; using OmniSharp.Extensions.JsonRpc; using OmniSharp.Extensions.LanguageServer.Protocol.General; +using OmniSharp.Extensions.LanguageServer.Protocol.Models; using OmniSharp.Extensions.LanguageServer.Protocol.Server; using OmniSharp.Extensions.LanguageServer.Server; @@ -138,12 +140,13 @@ public async Task StartAsync() languageServer.Services.GetService>() )); - // Set the workspace path from the parameters. + // Set the workspace path from the parameters. The collection may be + // absent (the field is optional in LSP), uninitialized, or contain + // entries without a URI, so we treat any of those as "no folders yet" + // rather than dereferencing a null and throwing. WorkspaceService workspaceService = languageServer.Services.GetService(); - if (initializeParams.WorkspaceFolders is not null) - { - workspaceService.WorkspaceFolders.AddRange(initializeParams.WorkspaceFolders); - } + workspaceService.WorkspaceFolders.AddRange( + GetValidWorkspaceFolders(initializeParams.WorkspaceFolders)); // Parse initialization options. JObject initializationOptions = initializeParams.InitializationOptions as JObject; @@ -161,7 +164,7 @@ public async Task StartAsync() // 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() + ?? workspaceService.WorkspaceFolders.FirstOrDefault()?.Uri?.GetFileSystemPath() ?? Directory.GetCurrentDirectory(), // If a shell integration script path is provided, that implies the feature is enabled. ShellIntegrationScript = initializationOptions?.GetValue("shellIntegrationScript")?.Value() @@ -180,6 +183,21 @@ public async Task StartAsync() _serverStart.SetResult(true); } + /// + /// Filters the workspace folders provided on initialize down to the usable ones. + /// + /// + /// The workspaceFolders field is optional in LSP, so the collection may be absent or + /// uninitialized, and individual folders may be sent without a URI. Any of those are treated + /// as "no folder" so that downstream code (which dereferences ) + /// does not throw a . + /// + /// The workspace folders from the initialize parameters. + /// The folders that have a non-null URI, or an empty sequence. + internal static IEnumerable GetValidWorkspaceFolders(IEnumerable workspaceFolders) + => workspaceFolders?.Where(static folder => folder?.Uri is not null) + ?? Enumerable.Empty(); + /// /// Get a task that completes when the server is shut down. /// diff --git a/test/PowerShellEditorServices.Test/Server/PsesLanguageServerTests.cs b/test/PowerShellEditorServices.Test/Server/PsesLanguageServerTests.cs new file mode 100644 index 000000000..507e3c04b --- /dev/null +++ b/test/PowerShellEditorServices.Test/Server/PsesLanguageServerTests.cs @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Linq; +using Microsoft.PowerShell.EditorServices.Server; +using OmniSharp.Extensions.LanguageServer.Protocol; +using OmniSharp.Extensions.LanguageServer.Protocol.Models; +using Xunit; + +namespace PowerShellEditorServices.Test.Server +{ + [Trait("Category", "Server")] + public class PsesLanguageServerTests + { + [Fact] + public void GetValidWorkspaceFoldersReturnsEmptyWhenNull() + => Assert.Empty(PsesLanguageServer.GetValidWorkspaceFolders(null)); + + [Fact] + public void GetValidWorkspaceFoldersReturnsEmptyWhenEmpty() + => Assert.Empty(PsesLanguageServer.GetValidWorkspaceFolders(new Container())); + + [Fact] + public void GetValidWorkspaceFoldersSkipsNullFoldersAndNullUris() + { + WorkspaceFolder valid = new() + { + Uri = DocumentUri.FromFileSystemPath("/home/runner/work/example"), + Name = "workspace" + }; + + Container folders = new( + null, + new WorkspaceFolder { Name = "missing-uri" }, + valid); + + WorkspaceFolder[] result = PsesLanguageServer.GetValidWorkspaceFolders(folders).ToArray(); + + Assert.Equal(valid, Assert.Single(result)); + } + } +}