Skip to content

Commit b0aec47

Browse files
Add completionItem/resolve request
1 parent 4b23fa0 commit b0aec47

File tree

4 files changed

+149
-10
lines changed

4 files changed

+149
-10
lines changed

src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ public async Task StartAsync()
6262
{
6363
_languageServer = await OS.LanguageServer.From(options => {
6464

65+
options.AddDefaultLoggingProvider();
66+
options.LoggerFactory = _configuration.LoggerFactory;
6567
ILogger logger = options.LoggerFactory.CreateLogger("OptionsStartup");
6668

6769
if (_configuration.Stdio)
@@ -89,10 +91,8 @@ public async Task StartAsync()
8991
options.Output = outNamedPipe ?? namedPipe;
9092
}
9193

92-
options.LoggerFactory = _configuration.LoggerFactory;
9394
options.MinimumLogLevel = _configuration.MinimumLogLevel;
9495
options.Services = _configuration.Services;
95-
9696
logger.LogInformation("Adding handlers");
9797

9898
options
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
//
2+
// Copyright (c) Microsoft. All rights reserved.
3+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
4+
//
5+
6+
using Microsoft.PowerShell.EditorServices.Utility;
7+
using System.Collections.Generic;
8+
using System.Linq;
9+
using System.Management.Automation;
10+
using System.Threading.Tasks;
11+
12+
namespace Microsoft.PowerShell.EditorServices
13+
{
14+
/// <summary>
15+
/// Provides utility methods for working with PowerShell commands.
16+
/// </summary>
17+
public static class CommandHelpers
18+
{
19+
private static readonly HashSet<string> NounBlackList =
20+
new HashSet<string>
21+
{
22+
"Module",
23+
"Script",
24+
"Package",
25+
"PackageProvider",
26+
"PackageSource",
27+
"InstalledModule",
28+
"InstalledScript",
29+
"ScriptFileInfo",
30+
"PSRepository"
31+
};
32+
33+
/// <summary>
34+
/// Gets the CommandInfo instance for a command with a particular name.
35+
/// </summary>
36+
/// <param name="commandName">The name of the command.</param>
37+
/// <param name="powerShellContext">The PowerShellContext to use for running Get-Command.</param>
38+
/// <returns>A CommandInfo object with details about the specified command.</returns>
39+
public static async Task<CommandInfo> GetCommandInfoAsync(
40+
string commandName,
41+
PowerShellContextService powerShellContext)
42+
{
43+
Validate.IsNotNull(nameof(commandName), commandName);
44+
45+
// Make sure the command's noun isn't blacklisted. This is
46+
// currently necessary to make sure that Get-Command doesn't
47+
// load PackageManagement or PowerShellGet because they cause
48+
// a major slowdown in IntelliSense.
49+
var commandParts = commandName.Split('-');
50+
if (commandParts.Length == 2 && NounBlackList.Contains(commandParts[1]))
51+
{
52+
return null;
53+
}
54+
55+
PSCommand command = new PSCommand();
56+
command.AddCommand(@"Microsoft.PowerShell.Core\Get-Command");
57+
command.AddArgument(commandName);
58+
command.AddParameter("ErrorAction", "Ignore");
59+
60+
return
61+
(await powerShellContext
62+
.ExecuteCommandAsync<PSObject>(command, false, false))
63+
.Select(o => o.BaseObject)
64+
.OfType<CommandInfo>()
65+
.FirstOrDefault();
66+
}
67+
68+
/// <summary>
69+
/// Gets the command's "Synopsis" documentation section.
70+
/// </summary>
71+
/// <param name="commandInfo">The CommandInfo instance for the command.</param>
72+
/// <param name="powerShellContext">The PowerShellContext to use for getting command documentation.</param>
73+
/// <returns></returns>
74+
public static async Task<string> GetCommandSynopsisAsync(
75+
CommandInfo commandInfo,
76+
PowerShellContextService powerShellContext)
77+
{
78+
string synopsisString = string.Empty;
79+
80+
if (commandInfo != null &&
81+
(commandInfo.CommandType == CommandTypes.Cmdlet ||
82+
commandInfo.CommandType == CommandTypes.Function ||
83+
commandInfo.CommandType == CommandTypes.Filter))
84+
{
85+
PSCommand command = new PSCommand();
86+
command.AddCommand(@"Microsoft.PowerShell.Core\Get-Help");
87+
command.AddArgument(commandInfo);
88+
command.AddParameter("ErrorAction", "Ignore");
89+
90+
var results = await powerShellContext.ExecuteCommandAsync<PSObject>(command, false, false);
91+
PSObject helpObject = results.FirstOrDefault();
92+
93+
if (helpObject != null)
94+
{
95+
// Extract the synopsis string from the object
96+
synopsisString =
97+
(string)helpObject.Properties["synopsis"].Value ??
98+
string.Empty;
99+
100+
// Ignore the placeholder value for this field
101+
if (string.Equals(synopsisString, "SHORT DESCRIPTION", System.StringComparison.CurrentCultureIgnoreCase))
102+
{
103+
synopsisString = string.Empty;
104+
}
105+
}
106+
}
107+
108+
return synopsisString;
109+
}
110+
}
111+
}

src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/CompletionHandler.cs

+27-2
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
namespace Microsoft.PowerShell.EditorServices.TextDocument
1818
{
19-
internal class CompletionHandler : ICompletionHandler
19+
internal class CompletionHandler : ICompletionHandler, ICompletionResolveHandler
2020
{
2121
const int DefaultWaitTimeoutMilliseconds = 5000;
2222
private readonly CompletionItem[] s_emptyCompletionResult = new CompletionItem[0];
@@ -84,6 +84,31 @@ await GetCompletionsInFileAsync(
8484
return new CompletionList(completionItems);
8585
}
8686

87+
public bool CanResolve(CompletionItem value)
88+
{
89+
return value.Kind == CompletionItemKind.Function;
90+
}
91+
92+
public async Task<CompletionItem> Handle(CompletionItem request, CancellationToken cancellationToken)
93+
{
94+
// Get the documentation for the function
95+
CommandInfo commandInfo =
96+
await CommandHelpers.GetCommandInfoAsync(
97+
request.Label,
98+
_powerShellContextService);
99+
100+
if (commandInfo != null)
101+
{
102+
request.Documentation =
103+
await CommandHelpers.GetCommandSynopsisAsync(
104+
commandInfo,
105+
_powerShellContextService);
106+
}
107+
108+
// Send back the updated CompletionItem
109+
return request;
110+
}
111+
87112
public void SetCapability(CompletionCapability capability)
88113
{
89114
_capability = capability;
@@ -214,7 +239,7 @@ private static CompletionItem CreateCompletionItem(
214239
}
215240
}
216241
else if ((completionDetails.CompletionType == CompletionType.Folder) &&
217-
(completionText.EndsWith("\"") || completionText.EndsWith("'")))
242+
(completionText.EndsWith("\"", StringComparison.OrdinalIgnoreCase) || completionText.EndsWith("'", StringComparison.OrdinalIgnoreCase)))
218243
{
219244
// Insert a final "tab stop" as identified by $0 in the snippet provided for completion.
220245
// For folder paths, we take the path returned by PowerShell e.g. 'C:\Program Files' and insert

test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs

+9-6
Original file line numberDiff line numberDiff line change
@@ -628,18 +628,21 @@ await LanguageClient.SendRequest<CommandOrCodeActionContainer>(
628628
}
629629

630630
[Fact]
631-
public async Task CanSendCompletionRequest()
631+
public async Task CanSendCompletionAndCompletionResolveRequest()
632632
{
633633
string filePath = NewTestFile("Write-H");
634634

635635
CompletionList completionItems = await LanguageClient.TextDocument.Completions(
636636
filePath, line: 0, column: 7);
637637

638-
Assert.Collection(completionItems,
639-
completionItem1 => {
640-
Assert.Equal("Write-Host", completionItem1.Label);
641-
}
642-
);
638+
CompletionItem completionItem = Assert.Single(completionItems,
639+
completionItem1 => completionItem1.Label == "Write-Host");
640+
641+
CompletionItem updatedCompletionItem = await LanguageClient.SendRequest<CompletionItem>(
642+
"completionItem/resolve",
643+
completionItem);
644+
645+
Assert.Contains("Writes customized output to a host", updatedCompletionItem.Documentation.String);
643646
}
644647
}
645648
}

0 commit comments

Comments
 (0)