Skip to content

Commit ba997b7

Browse files
completionresolve support (#1009)
* handle log messages * switch to using xUnit output helper * Add completionItem/resolve request * feedback * update build to run update-help for signature help test * removing scope hoping it works in CI * setting to EA silentlycontinue * change to language=powershell
1 parent 602c1fd commit ba997b7

File tree

15 files changed

+184
-31
lines changed

15 files changed

+184
-31
lines changed

scripts/azurePipelinesBuild.ps1

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ if ($IsWindows -or $PSVersionTable.PSVersion.Major -lt 6) {
1010
Import-Module -Name PackageManagement -MinimumVersion 1.1.7.0 -Force
1111
}
1212

13+
# Update help needed for SignatureHelp LSP request.
14+
Update-Help -Force -ErrorAction SilentlyContinue
15+
1316
# Needed for build and docs gen.
1417
Install-Module InvokeBuild -MaximumVersion 5.1.0 -Scope CurrentUser
1518
Install-Module PlatyPS -RequiredVersion 0.9.0 -Scope CurrentUser

src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs

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

src/PowerShellEditorServices.Engine/Services/Symbols/Vistors/AstOperations.cs

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ internal static class AstOperations
5555
/// A CommandCompletion instance that contains completions for the
5656
/// symbol at the given offset.
5757
/// </returns>
58-
static public async Task<CommandCompletion> GetCompletionsAsync(
58+
public static async Task<CommandCompletion> GetCompletionsAsync(
5959
Ast scriptAst,
6060
Token[] currentTokens,
6161
int fileOffset,
@@ -146,7 +146,7 @@ await powerShellContext.InvokeOnPipelineThreadAsync(
146146
/// <param name="columnNumber">The coulumn number of the cursor for the given script</param>
147147
/// <param name="includeFunctionDefinitions">Includes full function definition ranges in the search.</param>
148148
/// <returns>SymbolReference of found symbol</returns>
149-
static public SymbolReference FindSymbolAtPosition(
149+
public static SymbolReference FindSymbolAtPosition(
150150
Ast scriptAst,
151151
int lineNumber,
152152
int columnNumber,
@@ -170,7 +170,7 @@ static public SymbolReference FindSymbolAtPosition(
170170
/// <param name="lineNumber">The line number of the cursor for the given script</param>
171171
/// <param name="columnNumber">The column number of the cursor for the given script</param>
172172
/// <returns>SymbolReference of found command</returns>
173-
static public SymbolReference FindCommandAtPosition(Ast scriptAst, int lineNumber, int columnNumber)
173+
public static SymbolReference FindCommandAtPosition(Ast scriptAst, int lineNumber, int columnNumber)
174174
{
175175
FindCommandVisitor commandVisitor = new FindCommandVisitor(lineNumber, columnNumber);
176176
scriptAst.Visit(commandVisitor);
@@ -186,7 +186,7 @@ static public SymbolReference FindCommandAtPosition(Ast scriptAst, int lineNumbe
186186
/// <param name="CmdletToAliasDictionary">Dictionary maping cmdlets to aliases for finding alias references</param>
187187
/// <param name="AliasToCmdletDictionary">Dictionary maping aliases to cmdlets for finding alias references</param>
188188
/// <returns></returns>
189-
static public IEnumerable<SymbolReference> FindReferencesOfSymbol(
189+
public static IEnumerable<SymbolReference> FindReferencesOfSymbol(
190190
Ast scriptAst,
191191
SymbolReference symbolReference,
192192
Dictionary<String, List<String>> CmdletToAliasDictionary,
@@ -212,7 +212,7 @@ static public IEnumerable<SymbolReference> FindReferencesOfSymbol(
212212
/// This should always be false and used for occurence requests</param>
213213
/// <returns>A collection of SymbolReference objects that are refrences to the symbolRefrence
214214
/// not including aliases</returns>
215-
static public IEnumerable<SymbolReference> FindReferencesOfSymbol(
215+
public static IEnumerable<SymbolReference> FindReferencesOfSymbol(
216216
ScriptBlockAst scriptAst,
217217
SymbolReference foundSymbol,
218218
bool needsAliases)
@@ -230,7 +230,7 @@ static public IEnumerable<SymbolReference> FindReferencesOfSymbol(
230230
/// <param name="scriptAst">The abstract syntax tree of the given script</param>
231231
/// <param name="symbolReference">The symbol that we are looking for the definition of</param>
232232
/// <returns>A SymbolReference of the definition of the symbolReference</returns>
233-
static public SymbolReference FindDefinitionOfSymbol(
233+
public static SymbolReference FindDefinitionOfSymbol(
234234
Ast scriptAst,
235235
SymbolReference symbolReference)
236236
{
@@ -248,7 +248,7 @@ static public SymbolReference FindDefinitionOfSymbol(
248248
/// <param name="scriptAst">The abstract syntax tree of the given script</param>
249249
/// <param name="powerShellVersion">The PowerShell version the Ast was generated from</param>
250250
/// <returns>A collection of SymbolReference objects</returns>
251-
static public IEnumerable<SymbolReference> FindSymbolsInDocument(Ast scriptAst, Version powerShellVersion)
251+
public static IEnumerable<SymbolReference> FindSymbolsInDocument(Ast scriptAst, Version powerShellVersion)
252252
{
253253
IEnumerable<SymbolReference> symbolReferences = null;
254254

@@ -275,7 +275,7 @@ static public IEnumerable<SymbolReference> FindSymbolsInDocument(Ast scriptAst,
275275
/// </summary>
276276
/// <param name="ast">The abstract syntax tree of the given script</param>
277277
/// <returns>true if the AST represts a *.psd1 file, otherwise false</returns>
278-
static public bool IsPowerShellDataFileAst(Ast ast)
278+
public static bool IsPowerShellDataFileAst(Ast ast)
279279
{
280280
// sometimes we don't have reliable access to the filename
281281
// so we employ heuristics to check if the contents are
@@ -330,7 +330,7 @@ static private bool IsPowerShellDataFileAstNode(dynamic node, Type[] levelAstMap
330330
/// <param name="scriptAst">The abstract syntax tree of the given script</param>
331331
/// <param name="psScriptRoot">Pre-calculated value of $PSScriptRoot</param>
332332
/// <returns></returns>
333-
static public string[] FindDotSourcedIncludes(Ast scriptAst, string psScriptRoot)
333+
public static string[] FindDotSourcedIncludes(Ast scriptAst, string psScriptRoot)
334334
{
335335
FindDotSourcedVisitor dotSourcedVisitor = new FindDotSourcedVisitor(psScriptRoot);
336336
scriptAst.Visit(dotSourcedVisitor);

src/PowerShellEditorServices.Engine/Services/TextDocument/CompletionResults.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -243,8 +243,7 @@ internal static CompletionDetails Create(
243243
/// <returns>True if the CompletionResults instances have the same details.</returns>
244244
public override bool Equals(object obj)
245245
{
246-
CompletionDetails otherDetails = obj as CompletionDetails;
247-
if (otherDetails == null)
246+
if (!(obj is CompletionDetails otherDetails))
248247
{
249248
return false;
250249
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ public CodeActionHandler(ILoggerFactory factory, AnalysisService analysisService
3535
_analysisService = analysisService;
3636
_registrationOptions = new CodeActionRegistrationOptions()
3737
{
38-
DocumentSelector = new DocumentSelector(new DocumentFilter() { Pattern = "**/*.ps*1" }),
38+
DocumentSelector = new DocumentSelector(new DocumentFilter() { Language = "powershell" }),
3939
CodeActionKinds = s_supportedCodeActions
4040
};
4141
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ public class CodeLensHandlers : ICodeLensHandler, ICodeLensResolveHandler
1919
private readonly DocumentSelector _documentSelector = new DocumentSelector(
2020
new DocumentFilter()
2121
{
22-
Pattern = "**/*.ps*1"
22+
Language = "powershell"
2323
}
2424
);
2525

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

Lines changed: 40 additions & 4 deletions
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];
@@ -49,7 +49,7 @@ public CompletionRegistrationOptions GetRegistrationOptions()
4949
{
5050
return new CompletionRegistrationOptions
5151
{
52-
DocumentSelector = new DocumentSelector(new DocumentFilter { Pattern = "**/*.ps*1" }),
52+
DocumentSelector = new DocumentSelector(new DocumentFilter { Language = "powershell" }),
5353
ResolveProvider = true,
5454
TriggerCharacters = new[] { ".", "-", ":", "\\" }
5555
};
@@ -84,6 +84,32 @@ 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+
// Handler for "completionItem/resolve". In VSCode this is fired when a completion item is highlighted in the completion list.
93+
public async Task<CompletionItem> Handle(CompletionItem request, CancellationToken cancellationToken)
94+
{
95+
// Get the documentation for the function
96+
CommandInfo commandInfo =
97+
await CommandHelpers.GetCommandInfoAsync(
98+
request.Label,
99+
_powerShellContextService);
100+
101+
if (commandInfo != null)
102+
{
103+
request.Documentation =
104+
await CommandHelpers.GetCommandSynopsisAsync(
105+
commandInfo,
106+
_powerShellContextService);
107+
}
108+
109+
// Send back the updated CompletionItem
110+
return request;
111+
}
112+
87113
public void SetCapability(CompletionCapability capability)
88114
{
89115
_capability = capability;
@@ -213,8 +239,7 @@ private static CompletionItem CreateCompletionItem(
213239
}
214240
}
215241
}
216-
else if ((completionDetails.CompletionType == CompletionType.Folder) &&
217-
(completionText.EndsWith("\"") || completionText.EndsWith("'")))
242+
else if (completionDetails.CompletionType == CompletionType.Folder && EndsWithQuote(completionText))
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
@@ -291,5 +316,16 @@ private static CompletionItemKind MapCompletionKind(CompletionType completionTyp
291316
return CompletionItemKind.Text;
292317
}
293318
}
319+
320+
private static bool EndsWithQuote(string text)
321+
{
322+
if (string.IsNullOrEmpty(text))
323+
{
324+
return false;
325+
}
326+
327+
char lastChar = text[text.Length - 1];
328+
return lastChar == '"' || lastChar == '\'';
329+
}
294330
}
295331
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ public DocumentHighlightHandler(
3939
_symbolsService = symbolService;
4040
_registrationOptions = new TextDocumentRegistrationOptions()
4141
{
42-
DocumentSelector = new DocumentSelector(new DocumentFilter() { Pattern = "**/*.ps*1" } )
42+
DocumentSelector = new DocumentSelector(new DocumentFilter() { Language = "powershell" } )
4343
};
4444
_logger.LogInformation("highlight handler loaded");
4545
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ public class DocumentSymbolHandler : IDocumentSymbolHandler
2020
private readonly DocumentSelector _documentSelector = new DocumentSelector(
2121
new DocumentFilter()
2222
{
23-
Pattern = "**/*.ps*1"
23+
Language = "powershell"
2424
}
2525
);
2626

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ public class FoldingRangeHandler : IFoldingRangeHandler
1414
private readonly DocumentSelector _documentSelector = new DocumentSelector(
1515
new DocumentFilter()
1616
{
17-
Pattern = "**/*.ps*1"
17+
Language = "powershell"
1818
}
1919
);
2020

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ internal class DocumentFormattingHandler : IDocumentFormattingHandler
1515
private readonly DocumentSelector _documentSelector = new DocumentSelector(
1616
new DocumentFilter()
1717
{
18-
Pattern = "**/*.ps*1"
18+
Language = "powershell"
1919
}
2020
);
2121

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ class ReferencesHandler : IReferencesHandler
1616
private readonly DocumentSelector _documentSelector = new DocumentSelector(
1717
new DocumentFilter()
1818
{
19-
Pattern = "**/*.ps*1"
19+
Language = "powershell"
2020
}
2121
);
2222

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ class TextDocumentHandler : ITextDocumentSyncHandler
2424
private readonly DocumentSelector _documentSelector = new DocumentSelector(
2525
new DocumentFilter()
2626
{
27-
Pattern = "**/*.ps*1"
27+
Language = "powershell"
2828
}
2929
);
3030

0 commit comments

Comments
 (0)