Skip to content

Commit 9d56407

Browse files
authoredMar 4, 2025··
Surface Additional Parameter Documentation/Tooltips (#2222)
* Add Documentation to Parameters and Variables if Present * Add tests * Handle variables * Fix CompletesAttributeValue Test * Fix Rosyn Analyzer Nit on Liq Select * Change variable label handling
1 parent 8337c80 commit 9d56407

File tree

4 files changed

+74
-23
lines changed

4 files changed

+74
-23
lines changed
 

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

+36-7
Original file line numberDiff line numberDiff line change
@@ -327,15 +327,15 @@ CompletionResultType.ProviderItem or CompletionResultType.ProviderContainer
327327
Data = item.Detail,
328328
Detail = SupportsMarkdown ? null : item.Detail,
329329
},
330-
CompletionResultType.ParameterName => TryExtractType(detail, out string type)
331-
? item with { Kind = CompletionItemKind.Variable, Detail = type }
330+
CompletionResultType.ParameterName => TryExtractType(detail, item.Label, out string type, out string documentation)
331+
? item with { Kind = CompletionItemKind.Variable, Detail = type, Documentation = documentation }
332332
// The comparison operators (-eq, -not, -gt, etc) unfortunately come across as
333333
// ParameterName types but they don't have a type associated to them, so we can
334334
// deduce it is an operator.
335335
: item with { Kind = CompletionItemKind.Operator },
336336
CompletionResultType.ParameterValue => item with { Kind = CompletionItemKind.Value },
337-
CompletionResultType.Variable => TryExtractType(detail, out string type)
338-
? item with { Kind = CompletionItemKind.Variable, Detail = type }
337+
CompletionResultType.Variable => TryExtractType(detail, "$" + item.Label, out string type, out string documentation)
338+
? item with { Kind = CompletionItemKind.Variable, Detail = type, Documentation = documentation }
339339
: item with { Kind = CompletionItemKind.Variable },
340340
CompletionResultType.Namespace => item with { Kind = CompletionItemKind.Module },
341341
CompletionResultType.Type => detail.StartsWith("Class ", StringComparison.CurrentCulture)
@@ -450,16 +450,45 @@ private static string GetTypeFilterText(string textToBeReplaced, string completi
450450
/// type names in [] to be consistent with PowerShell syntax and how the debugger displays
451451
/// type names.
452452
/// </summary>
453-
/// <param name="toolTipText"></param>
454-
/// <param name="type"></param>
453+
/// <param name="toolTipText">The tooltip text to parse</param>
454+
/// <param name="type">The extracted type string, if found</param>
455+
/// <param name="documentation">The remaining text after the type, if any</param>
456+
/// <param name="label">The label to check for in the documentation prefix</param>
455457
/// <returns>Whether or not the type was found.</returns>
456-
private static bool TryExtractType(string toolTipText, out string type)
458+
internal static bool TryExtractType(string toolTipText, string label, out string type, out string documentation)
457459
{
458460
MatchCollection matches = s_typeRegex.Matches(toolTipText);
459461
type = string.Empty;
462+
documentation = null; //We use null instead of String.Empty to indicate no documentation was found.
463+
460464
if ((matches.Count > 0) && (matches[0].Groups.Count > 1))
461465
{
462466
type = matches[0].Groups[1].Value;
467+
468+
// Extract the description as everything after the type
469+
if (matches[0].Length < toolTipText.Length)
470+
{
471+
documentation = toolTipText.Substring(matches[0].Length).Trim();
472+
473+
if (documentation is not null)
474+
{
475+
// If the substring is the same as the label, documentation should remain blank
476+
if (documentation.Equals(label, StringComparison.OrdinalIgnoreCase))
477+
{
478+
documentation = null;
479+
}
480+
// If the documentation starts with "label - ", remove this prefix
481+
else if (documentation.StartsWith(label + " - ", StringComparison.OrdinalIgnoreCase))
482+
{
483+
documentation = documentation.Substring((label + " - ").Length).Trim();
484+
}
485+
}
486+
if (string.IsNullOrWhiteSpace(documentation))
487+
{
488+
documentation = null;
489+
}
490+
}
491+
463492
return true;
464493
}
465494
return false;

‎test/PowerShellEditorServices.Test.Shared/Completion/CompletionExamples.psm1

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ Import-Module PowerShellGet
1313
Get-Rand
1414

1515
function Test-Completion {
16-
param([Parameter(Mandatory, Value)])
16+
param([Parameter(Mandatory, Value)]$test)
1717
}
1818

1919
Get-ChildItem /

‎test/PowerShellEditorServices.Test/Language/CompletionHandlerTests.cs

+23-1
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,6 @@ public async Task CompletesVariableInFile()
105105
[SkippableFact]
106106
public async Task CompletesAttributeValue()
107107
{
108-
Skip.If(VersionUtils.IsPS74, "PowerShell 7.4 isn't returning these!");
109108
(_, IEnumerable<CompletionItem> results) = await GetCompletionResultsAsync(CompleteAttributeValue.SourceDetails);
110109
// NOTE: Since the completions come through un-ordered from PowerShell, their SortText
111110
// (which has an index prepended from the original order) will mis-match our assumed
@@ -126,5 +125,28 @@ public async Task CompletesFilePath()
126125
Assert.Equal(actual.TextEdit.TextEdit with { NewText = "" }, CompleteFilePath.ExpectedEdit);
127126
Assert.All(results, r => Assert.True(r.Kind is CompletionItemKind.File or CompletionItemKind.Folder));
128127
}
128+
129+
// TODO: These should be an integration tests at a higher level if/when https://github.com/PowerShell/PowerShell/pull/25108 is merged. As of today, we can't actually test this in the PS engine currently.
130+
[Fact]
131+
public void CanExtractTypeAndDescriptionFromTooltip()
132+
{
133+
string expectedType = "[string]";
134+
string expectedDescription = "Test String";
135+
string paramName = "TestParam";
136+
string testHelp = $"{expectedType} {paramName} - {expectedDescription}";
137+
Assert.True(PsesCompletionHandler.TryExtractType(testHelp, paramName, out string type, out string description));
138+
Assert.Equal(expectedType, type);
139+
Assert.Equal(expectedDescription, description);
140+
}
141+
142+
[Fact]
143+
public void CanExtractTypeFromTooltip()
144+
{
145+
string expectedType = "[string]";
146+
string testHelp = $"{expectedType}";
147+
Assert.True(PsesCompletionHandler.TryExtractType(testHelp, string.Empty, out string type, out string description));
148+
Assert.Null(description);
149+
Assert.Equal(expectedType, type);
150+
}
129151
}
130152
}

‎test/PowerShellEditorServices.Test/Language/SymbolsServiceTests.cs

+14-14
Original file line numberDiff line numberDiff line change
@@ -796,37 +796,37 @@ public void FindsSymbolsInFile()
796796
Assert.False(symbol.IsDeclaration);
797797
AssertIsRegion(symbol.NameRegion, 16, 29, 16, 39);
798798

799-
symbol = Assert.Single(symbols.Where(i => i.Type == SymbolType.Workflow));
799+
symbol = Assert.Single(symbols, i => i.Type == SymbolType.Workflow);
800800
Assert.Equal("fn AWorkflow", symbol.Id);
801801
Assert.Equal("workflow AWorkflow ()", symbol.Name);
802802
Assert.True(symbol.IsDeclaration);
803803

804-
symbol = Assert.Single(symbols.Where(i => i.Type == SymbolType.Class));
804+
symbol = Assert.Single(symbols, i => i.Type == SymbolType.Class);
805805
Assert.Equal("type AClass", symbol.Id);
806806
Assert.Equal("class AClass { }", symbol.Name);
807807
Assert.True(symbol.IsDeclaration);
808808

809-
symbol = Assert.Single(symbols.Where(i => i.Type == SymbolType.Property));
809+
symbol = Assert.Single(symbols, i => i.Type == SymbolType.Property);
810810
Assert.Equal("prop AProperty", symbol.Id);
811811
Assert.Equal("[string] $AProperty", symbol.Name);
812812
Assert.True(symbol.IsDeclaration);
813813

814-
symbol = Assert.Single(symbols.Where(i => i.Type == SymbolType.Constructor));
814+
symbol = Assert.Single(symbols, i => i.Type == SymbolType.Constructor);
815815
Assert.Equal("mtd AClass", symbol.Id);
816816
Assert.Equal("AClass([string]$AParameter)", symbol.Name);
817817
Assert.True(symbol.IsDeclaration);
818818

819-
symbol = Assert.Single(symbols.Where(i => i.Type == SymbolType.Method));
819+
symbol = Assert.Single(symbols, i => i.Type == SymbolType.Method);
820820
Assert.Equal("mtd AMethod", symbol.Id);
821821
Assert.Equal("void AMethod([string]$param1, [int]$param2, $param3)", symbol.Name);
822822
Assert.True(symbol.IsDeclaration);
823823

824-
symbol = Assert.Single(symbols.Where(i => i.Type == SymbolType.Enum));
824+
symbol = Assert.Single(symbols, i => i.Type == SymbolType.Enum);
825825
Assert.Equal("type AEnum", symbol.Id);
826826
Assert.Equal("enum AEnum { }", symbol.Name);
827827
Assert.True(symbol.IsDeclaration);
828828

829-
symbol = Assert.Single(symbols.Where(i => i.Type == SymbolType.EnumMember));
829+
symbol = Assert.Single(symbols, i => i.Type == SymbolType.EnumMember);
830830
Assert.Equal("prop AValue", symbol.Id);
831831
Assert.Equal("AValue", symbol.Name);
832832
Assert.True(symbol.IsDeclaration);
@@ -866,38 +866,38 @@ public void FindsSymbolsWithNewLineInFile()
866866
{
867867
IEnumerable<SymbolReference> symbols = FindSymbolsInFile(FindSymbolsInNewLineSymbolFile.SourceDetails);
868868

869-
SymbolReference symbol = Assert.Single(symbols.Where(i => i.Type == SymbolType.Function));
869+
SymbolReference symbol = Assert.Single(symbols, i => i.Type == SymbolType.Function);
870870
Assert.Equal("fn returnTrue", symbol.Id);
871871
AssertIsRegion(symbol.NameRegion, 2, 1, 2, 11);
872872
AssertIsRegion(symbol.ScriptRegion, 1, 1, 4, 2);
873873

874-
symbol = Assert.Single(symbols.Where(i => i.Type == SymbolType.Class));
874+
symbol = Assert.Single(symbols, i => i.Type == SymbolType.Class);
875875
Assert.Equal("type NewLineClass", symbol.Id);
876876
AssertIsRegion(symbol.NameRegion, 7, 1, 7, 13);
877877
AssertIsRegion(symbol.ScriptRegion, 6, 1, 23, 2);
878878

879-
symbol = Assert.Single(symbols.Where(i => i.Type == SymbolType.Constructor));
879+
symbol = Assert.Single(symbols, i => i.Type == SymbolType.Constructor);
880880
Assert.Equal("mtd NewLineClass", symbol.Id);
881881
AssertIsRegion(symbol.NameRegion, 8, 5, 8, 17);
882882
AssertIsRegion(symbol.ScriptRegion, 8, 5, 10, 6);
883883

884-
symbol = Assert.Single(symbols.Where(i => i.Type == SymbolType.Property));
884+
symbol = Assert.Single(symbols, i => i.Type == SymbolType.Property);
885885
Assert.Equal("prop SomePropWithDefault", symbol.Id);
886886
AssertIsRegion(symbol.NameRegion, 15, 5, 15, 25);
887887
AssertIsRegion(symbol.ScriptRegion, 12, 5, 15, 40);
888888

889-
symbol = Assert.Single(symbols.Where(i => i.Type == SymbolType.Method));
889+
symbol = Assert.Single(symbols, i => i.Type == SymbolType.Method);
890890
Assert.Equal("mtd MyClassMethod", symbol.Id);
891891
Assert.Equal("string MyClassMethod([MyNewLineEnum]$param1)", symbol.Name);
892892
AssertIsRegion(symbol.NameRegion, 20, 5, 20, 18);
893893
AssertIsRegion(symbol.ScriptRegion, 17, 5, 22, 6);
894894

895-
symbol = Assert.Single(symbols.Where(i => i.Type == SymbolType.Enum));
895+
symbol = Assert.Single(symbols, i => i.Type == SymbolType.Enum);
896896
Assert.Equal("type MyNewLineEnum", symbol.Id);
897897
AssertIsRegion(symbol.NameRegion, 26, 1, 26, 14);
898898
AssertIsRegion(symbol.ScriptRegion, 25, 1, 28, 2);
899899

900-
symbol = Assert.Single(symbols.Where(i => i.Type == SymbolType.EnumMember));
900+
symbol = Assert.Single(symbols, i => i.Type == SymbolType.EnumMember);
901901
Assert.Equal("prop First", symbol.Id);
902902
AssertIsRegion(symbol.NameRegion, 27, 5, 27, 10);
903903
AssertIsRegion(symbol.ScriptRegion, 27, 5, 27, 10);

0 commit comments

Comments
 (0)
Please sign in to comment.