diff --git a/src/PowerShellEditorServices/Services/TextDocument/Handlers/CompletionHandler.cs b/src/PowerShellEditorServices/Services/TextDocument/Handlers/CompletionHandler.cs index 29e36ce25..c3d7a39c5 100644 --- a/src/PowerShellEditorServices/Services/TextDocument/Handlers/CompletionHandler.cs +++ b/src/PowerShellEditorServices/Services/TextDocument/Handlers/CompletionHandler.cs @@ -327,15 +327,15 @@ CompletionResultType.ProviderItem or CompletionResultType.ProviderContainer Data = item.Detail, Detail = SupportsMarkdown ? null : item.Detail, }, - CompletionResultType.ParameterName => TryExtractType(detail, out string type) - ? item with { Kind = CompletionItemKind.Variable, Detail = type } + CompletionResultType.ParameterName => TryExtractType(detail, item.Label, out string type, out string documentation) + ? item with { Kind = CompletionItemKind.Variable, Detail = type, Documentation = documentation } // The comparison operators (-eq, -not, -gt, etc) unfortunately come across as // ParameterName types but they don't have a type associated to them, so we can // deduce it is an operator. : item with { Kind = CompletionItemKind.Operator }, CompletionResultType.ParameterValue => item with { Kind = CompletionItemKind.Value }, - CompletionResultType.Variable => TryExtractType(detail, out string type) - ? item with { Kind = CompletionItemKind.Variable, Detail = type } + CompletionResultType.Variable => TryExtractType(detail, "$" + item.Label, out string type, out string documentation) + ? item with { Kind = CompletionItemKind.Variable, Detail = type, Documentation = documentation } : item with { Kind = CompletionItemKind.Variable }, CompletionResultType.Namespace => item with { Kind = CompletionItemKind.Module }, CompletionResultType.Type => detail.StartsWith("Class ", StringComparison.CurrentCulture) @@ -450,16 +450,45 @@ private static string GetTypeFilterText(string textToBeReplaced, string completi /// type names in [] to be consistent with PowerShell syntax and how the debugger displays /// type names. /// - /// - /// + /// The tooltip text to parse + /// The extracted type string, if found + /// The remaining text after the type, if any + /// The label to check for in the documentation prefix /// Whether or not the type was found. - private static bool TryExtractType(string toolTipText, out string type) + internal static bool TryExtractType(string toolTipText, string label, out string type, out string documentation) { MatchCollection matches = s_typeRegex.Matches(toolTipText); type = string.Empty; + documentation = null; //We use null instead of String.Empty to indicate no documentation was found. + if ((matches.Count > 0) && (matches[0].Groups.Count > 1)) { type = matches[0].Groups[1].Value; + + // Extract the description as everything after the type + if (matches[0].Length < toolTipText.Length) + { + documentation = toolTipText.Substring(matches[0].Length).Trim(); + + if (documentation is not null) + { + // If the substring is the same as the label, documentation should remain blank + if (documentation.Equals(label, StringComparison.OrdinalIgnoreCase)) + { + documentation = null; + } + // If the documentation starts with "label - ", remove this prefix + else if (documentation.StartsWith(label + " - ", StringComparison.OrdinalIgnoreCase)) + { + documentation = documentation.Substring((label + " - ").Length).Trim(); + } + } + if (string.IsNullOrWhiteSpace(documentation)) + { + documentation = null; + } + } + return true; } return false; diff --git a/test/PowerShellEditorServices.Test.Shared/Completion/CompletionExamples.psm1 b/test/PowerShellEditorServices.Test.Shared/Completion/CompletionExamples.psm1 index b8427615e..840ec9fdf 100644 --- a/test/PowerShellEditorServices.Test.Shared/Completion/CompletionExamples.psm1 +++ b/test/PowerShellEditorServices.Test.Shared/Completion/CompletionExamples.psm1 @@ -13,7 +13,7 @@ Import-Module PowerShellGet Get-Rand function Test-Completion { - param([Parameter(Mandatory, Value)]) + param([Parameter(Mandatory, Value)]$test) } Get-ChildItem / diff --git a/test/PowerShellEditorServices.Test/Language/CompletionHandlerTests.cs b/test/PowerShellEditorServices.Test/Language/CompletionHandlerTests.cs index 35546cefe..fb8076476 100644 --- a/test/PowerShellEditorServices.Test/Language/CompletionHandlerTests.cs +++ b/test/PowerShellEditorServices.Test/Language/CompletionHandlerTests.cs @@ -105,7 +105,6 @@ public async Task CompletesVariableInFile() [SkippableFact] public async Task CompletesAttributeValue() { - Skip.If(VersionUtils.IsPS74, "PowerShell 7.4 isn't returning these!"); (_, IEnumerable results) = await GetCompletionResultsAsync(CompleteAttributeValue.SourceDetails); // NOTE: Since the completions come through un-ordered from PowerShell, their SortText // (which has an index prepended from the original order) will mis-match our assumed @@ -126,5 +125,28 @@ public async Task CompletesFilePath() Assert.Equal(actual.TextEdit.TextEdit with { NewText = "" }, CompleteFilePath.ExpectedEdit); Assert.All(results, r => Assert.True(r.Kind is CompletionItemKind.File or CompletionItemKind.Folder)); } + + // 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. + [Fact] + public void CanExtractTypeAndDescriptionFromTooltip() + { + string expectedType = "[string]"; + string expectedDescription = "Test String"; + string paramName = "TestParam"; + string testHelp = $"{expectedType} {paramName} - {expectedDescription}"; + Assert.True(PsesCompletionHandler.TryExtractType(testHelp, paramName, out string type, out string description)); + Assert.Equal(expectedType, type); + Assert.Equal(expectedDescription, description); + } + + [Fact] + public void CanExtractTypeFromTooltip() + { + string expectedType = "[string]"; + string testHelp = $"{expectedType}"; + Assert.True(PsesCompletionHandler.TryExtractType(testHelp, string.Empty, out string type, out string description)); + Assert.Null(description); + Assert.Equal(expectedType, type); + } } } diff --git a/test/PowerShellEditorServices.Test/Language/SymbolsServiceTests.cs b/test/PowerShellEditorServices.Test/Language/SymbolsServiceTests.cs index 359a424f8..28a51ae3a 100644 --- a/test/PowerShellEditorServices.Test/Language/SymbolsServiceTests.cs +++ b/test/PowerShellEditorServices.Test/Language/SymbolsServiceTests.cs @@ -796,37 +796,37 @@ public void FindsSymbolsInFile() Assert.False(symbol.IsDeclaration); AssertIsRegion(symbol.NameRegion, 16, 29, 16, 39); - symbol = Assert.Single(symbols.Where(i => i.Type == SymbolType.Workflow)); + symbol = Assert.Single(symbols, i => i.Type == SymbolType.Workflow); Assert.Equal("fn AWorkflow", symbol.Id); Assert.Equal("workflow AWorkflow ()", symbol.Name); Assert.True(symbol.IsDeclaration); - symbol = Assert.Single(symbols.Where(i => i.Type == SymbolType.Class)); + symbol = Assert.Single(symbols, i => i.Type == SymbolType.Class); Assert.Equal("type AClass", symbol.Id); Assert.Equal("class AClass { }", symbol.Name); Assert.True(symbol.IsDeclaration); - symbol = Assert.Single(symbols.Where(i => i.Type == SymbolType.Property)); + symbol = Assert.Single(symbols, i => i.Type == SymbolType.Property); Assert.Equal("prop AProperty", symbol.Id); Assert.Equal("[string] $AProperty", symbol.Name); Assert.True(symbol.IsDeclaration); - symbol = Assert.Single(symbols.Where(i => i.Type == SymbolType.Constructor)); + symbol = Assert.Single(symbols, i => i.Type == SymbolType.Constructor); Assert.Equal("mtd AClass", symbol.Id); Assert.Equal("AClass([string]$AParameter)", symbol.Name); Assert.True(symbol.IsDeclaration); - symbol = Assert.Single(symbols.Where(i => i.Type == SymbolType.Method)); + symbol = Assert.Single(symbols, i => i.Type == SymbolType.Method); Assert.Equal("mtd AMethod", symbol.Id); Assert.Equal("void AMethod([string]$param1, [int]$param2, $param3)", symbol.Name); Assert.True(symbol.IsDeclaration); - symbol = Assert.Single(symbols.Where(i => i.Type == SymbolType.Enum)); + symbol = Assert.Single(symbols, i => i.Type == SymbolType.Enum); Assert.Equal("type AEnum", symbol.Id); Assert.Equal("enum AEnum { }", symbol.Name); Assert.True(symbol.IsDeclaration); - symbol = Assert.Single(symbols.Where(i => i.Type == SymbolType.EnumMember)); + symbol = Assert.Single(symbols, i => i.Type == SymbolType.EnumMember); Assert.Equal("prop AValue", symbol.Id); Assert.Equal("AValue", symbol.Name); Assert.True(symbol.IsDeclaration); @@ -866,38 +866,38 @@ public void FindsSymbolsWithNewLineInFile() { IEnumerable symbols = FindSymbolsInFile(FindSymbolsInNewLineSymbolFile.SourceDetails); - SymbolReference symbol = Assert.Single(symbols.Where(i => i.Type == SymbolType.Function)); + SymbolReference symbol = Assert.Single(symbols, i => i.Type == SymbolType.Function); Assert.Equal("fn returnTrue", symbol.Id); AssertIsRegion(symbol.NameRegion, 2, 1, 2, 11); AssertIsRegion(symbol.ScriptRegion, 1, 1, 4, 2); - symbol = Assert.Single(symbols.Where(i => i.Type == SymbolType.Class)); + symbol = Assert.Single(symbols, i => i.Type == SymbolType.Class); Assert.Equal("type NewLineClass", symbol.Id); AssertIsRegion(symbol.NameRegion, 7, 1, 7, 13); AssertIsRegion(symbol.ScriptRegion, 6, 1, 23, 2); - symbol = Assert.Single(symbols.Where(i => i.Type == SymbolType.Constructor)); + symbol = Assert.Single(symbols, i => i.Type == SymbolType.Constructor); Assert.Equal("mtd NewLineClass", symbol.Id); AssertIsRegion(symbol.NameRegion, 8, 5, 8, 17); AssertIsRegion(symbol.ScriptRegion, 8, 5, 10, 6); - symbol = Assert.Single(symbols.Where(i => i.Type == SymbolType.Property)); + symbol = Assert.Single(symbols, i => i.Type == SymbolType.Property); Assert.Equal("prop SomePropWithDefault", symbol.Id); AssertIsRegion(symbol.NameRegion, 15, 5, 15, 25); AssertIsRegion(symbol.ScriptRegion, 12, 5, 15, 40); - symbol = Assert.Single(symbols.Where(i => i.Type == SymbolType.Method)); + symbol = Assert.Single(symbols, i => i.Type == SymbolType.Method); Assert.Equal("mtd MyClassMethod", symbol.Id); Assert.Equal("string MyClassMethod([MyNewLineEnum]$param1)", symbol.Name); AssertIsRegion(symbol.NameRegion, 20, 5, 20, 18); AssertIsRegion(symbol.ScriptRegion, 17, 5, 22, 6); - symbol = Assert.Single(symbols.Where(i => i.Type == SymbolType.Enum)); + symbol = Assert.Single(symbols, i => i.Type == SymbolType.Enum); Assert.Equal("type MyNewLineEnum", symbol.Id); AssertIsRegion(symbol.NameRegion, 26, 1, 26, 14); AssertIsRegion(symbol.ScriptRegion, 25, 1, 28, 2); - symbol = Assert.Single(symbols.Where(i => i.Type == SymbolType.EnumMember)); + symbol = Assert.Single(symbols, i => i.Type == SymbolType.EnumMember); Assert.Equal("prop First", symbol.Id); AssertIsRegion(symbol.NameRegion, 27, 5, 27, 10); AssertIsRegion(symbol.ScriptRegion, 27, 5, 27, 10);