diff --git a/src/PowerShellEditorServices.Host/LanguageServer.cs b/src/PowerShellEditorServices.Host/LanguageServer.cs index 105808f8a..7abf52cc4 100644 --- a/src/PowerShellEditorServices.Host/LanguageServer.cs +++ b/src/PowerShellEditorServices.Host/LanguageServer.cs @@ -16,6 +16,7 @@ using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; +using System.IO; namespace Microsoft.PowerShell.EditorServices.Host { @@ -535,14 +536,71 @@ protected async Task HandleDocumentSymbolRequest( EditorSession editorSession, RequestContext requestContext) { - // TODO: Implement this with Keith's changes. + ScriptFile scriptFile = + editorSession.Workspace.GetFile( + textDocumentIdentifier.Uri); + + FindOccurrencesResult foundSymbols = + editorSession.LanguageService.FindSymbolsInFile( + scriptFile); + + SymbolInformation[] symbols = null; - // NOTE SymbolInformation.Location's Start/End Position are - // zero-based while the LanguageService APIs are one-based. - // Make sure to subtract line/column positions by 1 when creating - // the result list. + string containerName = Path.GetFileNameWithoutExtension(scriptFile.FilePath); + + if (foundSymbols != null) + { + symbols = + foundSymbols + .FoundOccurrences + .Select(r => + { + return new SymbolInformation { + ContainerName = containerName, + Kind = GetSymbolKind(r.SymbolType), + Location = new Location { + Uri = new Uri(r.FilePath).AbsolutePath, + Range = GetRangeFromScriptRegion(r.ScriptRegion) + }, + Name = GetDecoratedSymbolName(r) + }; + }) + .ToArray(); + } + else + { + symbols = new SymbolInformation[0]; + } - await requestContext.SendResult(new SymbolInformation[0]); + await requestContext.SendResult(symbols); + } + + private SymbolKind GetSymbolKind(SymbolType symbolType) + { + switch (symbolType) + { + case SymbolType.Configuration: + case SymbolType.Function: + case SymbolType.Workflow: + return SymbolKind.Function; + + default: + return SymbolKind.Variable; + } + } + + private string GetDecoratedSymbolName(SymbolReference symbolReference) + { + string name = symbolReference.SymbolName; + + if (symbolReference.SymbolType == SymbolType.Configuration || + symbolReference.SymbolType == SymbolType.Function || + symbolReference.SymbolType == SymbolType.Workflow) + { + name += " { }"; + } + + return name; } protected async Task HandleWorkspaceSymbolRequest( @@ -550,14 +608,47 @@ protected async Task HandleWorkspaceSymbolRequest( EditorSession editorSession, RequestContext requestContext) { - // TODO: Implement this with Keith's changes + var symbols = new List(); - // NOTE SymbolInformation.Location's Start/End Position are - // zero-based while the LanguageService APIs are one-based. - // Make sure to subtract line/column positions by 1 when creating - // the result list. + foreach (ScriptFile scriptFile in editorSession.Workspace.GetOpenedFiles()) + { + FindOccurrencesResult foundSymbols = + editorSession.LanguageService.FindSymbolsInFile( + scriptFile); - await requestContext.SendResult(new SymbolInformation[0]); + // TODO: Need to compute a relative path that is based on common path for all workspace files + string containerName = Path.GetFileNameWithoutExtension(scriptFile.FilePath); + + if (foundSymbols != null) + { + var matchedSymbols = + foundSymbols + .FoundOccurrences + .Where(r => IsQueryMatch(workspaceSymbolParams.Query, r.SymbolName)) + .Select(r => + { + return new SymbolInformation + { + ContainerName = containerName, + Kind = r.SymbolType == SymbolType.Variable ? SymbolKind.Variable : SymbolKind.Function, + Location = new Location { + Uri = new Uri(r.FilePath).AbsoluteUri, + Range = GetRangeFromScriptRegion(r.ScriptRegion) + }, + Name = GetDecoratedSymbolName(r) + }; + }); + + symbols.AddRange(matchedSymbols); + } + } + + await requestContext.SendResult(symbols.ToArray()); + } + + private bool IsQueryMatch(string query, string symbolName) + { + return symbolName.IndexOf(query, StringComparison.OrdinalIgnoreCase) >= 0; } protected async Task HandleEvaluateRequest( diff --git a/src/PowerShellEditorServices/Language/AstOperations.cs b/src/PowerShellEditorServices/Language/AstOperations.cs index 223f8c7f9..1d1178651 100644 --- a/src/PowerShellEditorServices/Language/AstOperations.cs +++ b/src/PowerShellEditorServices/Language/AstOperations.cs @@ -131,8 +131,8 @@ static public IEnumerable FindReferencesOfSymbol( scriptAst.Visit(referencesVisitor); return referencesVisitor.FoundReferences; - } + /// /// Finds all references (not including aliases) in a script for the given symbol /// @@ -172,6 +172,19 @@ static public SymbolReference FindDefinitionOfSymbol( return declarationVisitor.FoundDeclartion; } + /// + /// Finds all symbols in a script + /// + /// The abstract syntax tree of the given script + /// A collection of SymbolReference objects + static public IEnumerable FindSymbolsInDocument(Ast scriptAst) + { + FindSymbolsVisitor findSymbolsVisitor = new FindSymbolsVisitor(); + scriptAst.Visit(findSymbolsVisitor); + + return findSymbolsVisitor.SymbolReferences; + } + /// /// Finds all files dot sourced in a script /// diff --git a/src/PowerShellEditorServices/Language/FindSymbolsVisitor.cs b/src/PowerShellEditorServices/Language/FindSymbolsVisitor.cs new file mode 100644 index 000000000..fdafa593a --- /dev/null +++ b/src/PowerShellEditorServices/Language/FindSymbolsVisitor.cs @@ -0,0 +1,107 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Collections.Generic; +using System.Management.Automation.Language; + +namespace Microsoft.PowerShell.EditorServices +{ + /// + /// The visitor used to find all the symbols (function and class defs) in the AST. + /// + internal class FindSymbolsVisitor : AstVisitor2 + { + public List SymbolReferences { get; private set; } + + public FindSymbolsVisitor() + { + this.SymbolReferences = new List(); + } + + /// + /// Adds each function defintion as a + /// + /// A functionDefinitionAst object in the script's AST + /// A decision to stop searching if the right symbol was found, + /// or a decision to continue if it wasn't found + public override AstVisitAction VisitFunctionDefinition(FunctionDefinitionAst functionDefinitionAst) + { + IScriptExtent nameExtent = new ScriptExtent() { + Text = functionDefinitionAst.Name, + StartLineNumber = functionDefinitionAst.Extent.StartLineNumber, + EndLineNumber = functionDefinitionAst.Extent.EndLineNumber, + StartColumnNumber = functionDefinitionAst.Extent.StartColumnNumber, + EndColumnNumber = functionDefinitionAst.Extent.EndColumnNumber + }; + + SymbolType symbolType = + functionDefinitionAst.IsWorkflow ? + SymbolType.Workflow : SymbolType.Function; + + this.SymbolReferences.Add( + new SymbolReference( + symbolType, + nameExtent)); + + return AstVisitAction.Continue; + } + + /// + /// Checks to see if this variable expression is the symbol we are looking for. + /// + /// A VariableExpressionAst object in the script's AST + /// A descion to stop searching if the right symbol was found, + /// or a decision to continue if it wasn't found + public override AstVisitAction VisitVariableExpression(VariableExpressionAst variableExpressionAst) + { + if (!IsAssignedAtScriptScope(variableExpressionAst)) + { + return AstVisitAction.Continue; + } + + this.SymbolReferences.Add( + new SymbolReference( + SymbolType.Variable, + variableExpressionAst.Extent)); + + return AstVisitAction.Continue; + } + + public override AstVisitAction VisitConfigurationDefinition(ConfigurationDefinitionAst configurationDefinitionAst) + { + IScriptExtent nameExtent = new ScriptExtent() { + Text = configurationDefinitionAst.InstanceName.Extent.Text, + StartLineNumber = configurationDefinitionAst.Extent.StartLineNumber, + EndLineNumber = configurationDefinitionAst.Extent.EndLineNumber, + StartColumnNumber = configurationDefinitionAst.Extent.StartColumnNumber, + EndColumnNumber = configurationDefinitionAst.Extent.EndColumnNumber + }; + + this.SymbolReferences.Add( + new SymbolReference( + SymbolType.Configuration, + nameExtent)); + + return AstVisitAction.Continue; + } + + private bool IsAssignedAtScriptScope(VariableExpressionAst variableExpressionAst) + { + Ast parent = variableExpressionAst.Parent; + if (!(parent is AssignmentStatementAst)) + { + return false; + } + + parent = parent.Parent; + if (parent == null || parent.Parent == null || parent.Parent.Parent == null) + { + return true; + } + + return false; + } + } +} diff --git a/src/PowerShellEditorServices/Language/LanguageService.cs b/src/PowerShellEditorServices/Language/LanguageService.cs index cabb0a5f4..61c0ccd23 100644 --- a/src/PowerShellEditorServices/Language/LanguageService.cs +++ b/src/PowerShellEditorServices/Language/LanguageService.cs @@ -198,6 +198,32 @@ public async Task FindSymbolDetailsAtLocation( return symbolDetails; } + /// + /// Finds all the symbols in a file. + /// + /// The ScriptFile in which the symbol can be located. + /// + public FindOccurrencesResult FindSymbolsInFile(ScriptFile scriptFile) + { + Validate.IsNotNull("scriptFile", scriptFile); + + IEnumerable symbolReferencesinFile = + AstOperations + .FindSymbolsInDocument(scriptFile.ScriptAst) + .Select( + reference => { + reference.SourceLine = + scriptFile.GetLine(reference.ScriptRegion.StartLineNumber); + reference.FilePath = scriptFile.FilePath; + return reference; + }); + + return + new FindOccurrencesResult { + FoundOccurrences = symbolReferencesinFile + }; + } + /// /// Finds all the references of a symbol /// diff --git a/src/PowerShellEditorServices/Language/SymbolType.cs b/src/PowerShellEditorServices/Language/SymbolType.cs index e93b9e83f..c01592f1e 100644 --- a/src/PowerShellEditorServices/Language/SymbolType.cs +++ b/src/PowerShellEditorServices/Language/SymbolType.cs @@ -28,8 +28,17 @@ public enum SymbolType /// /// The symbol is a parameter /// - Parameter - } + Parameter, + + /// + /// The symbol is a DSC configuration + /// + Configuration, + /// + /// The symbol is a workflow + /// + Workflow, + } } diff --git a/src/PowerShellEditorServices/PowerShellEditorServices.csproj b/src/PowerShellEditorServices/PowerShellEditorServices.csproj index 57e8fe311..38a959ddf 100644 --- a/src/PowerShellEditorServices/PowerShellEditorServices.csproj +++ b/src/PowerShellEditorServices/PowerShellEditorServices.csproj @@ -74,6 +74,7 @@ + diff --git a/src/PowerShellEditorServices/Workspace/Workspace.cs b/src/PowerShellEditorServices/Workspace/Workspace.cs index 1a77b1986..3cac4052e 100644 --- a/src/PowerShellEditorServices/Workspace/Workspace.cs +++ b/src/PowerShellEditorServices/Workspace/Workspace.cs @@ -91,6 +91,13 @@ public ScriptFile GetFileBuffer(string filePath, string initialBuffer) return scriptFile; } + public ScriptFile[] GetOpenedFiles() + { + var scriptFiles = new ScriptFile[workspaceFiles.Count]; + workspaceFiles.Values.CopyTo(scriptFiles, 0); + return scriptFiles; + } + /// /// Closes a currently open script file with the given file path. /// diff --git a/test/PowerShellEditorServices.Test.Shared/PowerShellEditorServices.Test.Shared.csproj b/test/PowerShellEditorServices.Test.Shared/PowerShellEditorServices.Test.Shared.csproj index 1a1d89025..e74f34398 100644 --- a/test/PowerShellEditorServices.Test.Shared/PowerShellEditorServices.Test.Shared.csproj +++ b/test/PowerShellEditorServices.Test.Shared/PowerShellEditorServices.Test.Shared.csproj @@ -55,6 +55,8 @@ + + @@ -62,6 +64,8 @@ + + diff --git a/test/PowerShellEditorServices.Test.Shared/Symbols/FindSymbolsInMultiSymbolFile.cs b/test/PowerShellEditorServices.Test.Shared/Symbols/FindSymbolsInMultiSymbolFile.cs new file mode 100644 index 000000000..d3904f1fe --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/Symbols/FindSymbolsInMultiSymbolFile.cs @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +namespace Microsoft.PowerShell.EditorServices.Test.Shared.Symbols +{ + public class FindSymbolsInMultiSymbolFile + { + public static readonly ScriptRegion SourceDetails = + new ScriptRegion { + File = @"Symbols\MultipleSymbols.ps1" + }; + } +} diff --git a/test/PowerShellEditorServices.Test.Shared/Symbols/FindSymbolsInNoSymbolsFile.cs b/test/PowerShellEditorServices.Test.Shared/Symbols/FindSymbolsInNoSymbolsFile.cs new file mode 100644 index 000000000..638894999 --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/Symbols/FindSymbolsInNoSymbolsFile.cs @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +namespace Microsoft.PowerShell.EditorServices.Test.Shared.Symbols +{ + public class FindSymbolsInNoSymbolsFile + { + public static readonly ScriptRegion SourceDetails = + new ScriptRegion { + File = @"Symbols\NoSymbols.ps1" + }; + } +} diff --git a/test/PowerShellEditorServices.Test.Shared/Symbols/MultipleSymbols.ps1 b/test/PowerShellEditorServices.Test.Shared/Symbols/MultipleSymbols.ps1 new file mode 100644 index 000000000..f234fed03 --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/Symbols/MultipleSymbols.ps1 @@ -0,0 +1,31 @@ +$Global:GlobalVar = 0 +$UnqualifiedScriptVar = 1 +$Script:ScriptVar2 = 2 + +"`$Script:ScriptVar2 is $Script:ScriptVar2" + +function AFunction {} + +filter AFilter {$_} + +function AnAdvancedFunction { + begin { + $LocalVar = 'LocalVar' + function ANestedFunction() { + $nestedVar = 42 + "`$nestedVar is $nestedVar" + } + } + process {} + end {} +} + +workflow AWorkflow {} + +Configuration AConfiguration { + Node "TEST-PC" {} +} + +AFunction +1..3 | AFilter +AnAdvancedFunction \ No newline at end of file diff --git a/test/PowerShellEditorServices.Test.Shared/Symbols/NoSymbols.ps1 b/test/PowerShellEditorServices.Test.Shared/Symbols/NoSymbols.ps1 new file mode 100644 index 000000000..3582fde41 --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/Symbols/NoSymbols.ps1 @@ -0,0 +1 @@ +# This file represents a script with no symbols diff --git a/test/PowerShellEditorServices.Test/Language/LanguageServiceTests.cs b/test/PowerShellEditorServices.Test/Language/LanguageServiceTests.cs index 49b5b7f7e..3538e22c9 100644 --- a/test/PowerShellEditorServices.Test/Language/LanguageServiceTests.cs +++ b/test/PowerShellEditorServices.Test/Language/LanguageServiceTests.cs @@ -9,6 +9,7 @@ using Microsoft.PowerShell.EditorServices.Test.Shared.ParameterHint; using Microsoft.PowerShell.EditorServices.Test.Shared.References; using Microsoft.PowerShell.EditorServices.Test.Shared.SymbolDetails; +using Microsoft.PowerShell.EditorServices.Test.Shared.Symbols; using System; using System.IO; using System.Linq; @@ -224,7 +225,44 @@ await this.languageService.FindSymbolDetailsAtLocation( Assert.NotNull(symbolDetails.Documentation); Assert.NotEqual("", symbolDetails.Documentation); } - + + [Fact] + public void LanguageServiceFindsSymbolsInFile() + { + FindOccurrencesResult symbolsResult = + this.FindSymbolsInFile( + FindSymbolsInMultiSymbolFile.SourceDetails); + + Assert.Equal(4, symbolsResult.FoundOccurrences.Where(r => r.SymbolType == SymbolType.Function).Count()); + Assert.Equal(3, symbolsResult.FoundOccurrences.Where(r => r.SymbolType == SymbolType.Variable).Count()); + Assert.Equal(1, symbolsResult.FoundOccurrences.Where(r => r.SymbolType == SymbolType.Workflow).Count()); + + SymbolReference firstFunctionSymbol = symbolsResult.FoundOccurrences.Where(r => r.SymbolType == SymbolType.Function).First(); + Assert.Equal("AFunction", firstFunctionSymbol.SymbolName); + Assert.Equal(7, firstFunctionSymbol.ScriptRegion.StartLineNumber); + Assert.Equal(1, firstFunctionSymbol.ScriptRegion.StartColumnNumber); + + SymbolReference lastVariableSymbol = symbolsResult.FoundOccurrences.Where(r => r.SymbolType == SymbolType.Variable).Last(); + Assert.Equal("$Script:ScriptVar2", lastVariableSymbol.SymbolName); + Assert.Equal(3, lastVariableSymbol.ScriptRegion.StartLineNumber); + Assert.Equal(1, lastVariableSymbol.ScriptRegion.StartColumnNumber); + + SymbolReference firstWorkflowSymbol = symbolsResult.FoundOccurrences.Where(r => r.SymbolType == SymbolType.Workflow).First(); + Assert.Equal("AWorkflow", firstWorkflowSymbol.SymbolName); + Assert.Equal(23, firstWorkflowSymbol.ScriptRegion.StartLineNumber); + Assert.Equal(1, firstWorkflowSymbol.ScriptRegion.StartColumnNumber); + } + + [Fact] + public void LanguageServiceFindsSymbolsInNoSymbolsFile() + { + FindOccurrencesResult symbolsResult = + this.FindSymbolsInFile( + FindSymbolsInNoSymbolsFile.SourceDetails); + + Assert.Equal(0, symbolsResult.FoundOccurrences.Count()); + } + private ScriptFile GetScriptFile(ScriptRegion scriptRegion) { const string baseSharedScriptPath = @@ -304,5 +342,12 @@ private FindOccurrencesResult GetOccurrences(ScriptRegion scriptRegion) scriptRegion.StartLineNumber, scriptRegion.StartColumnNumber); } + + private FindOccurrencesResult FindSymbolsInFile(ScriptRegion scriptRegion) + { + return + this.languageService.FindSymbolsInFile( + GetScriptFile(scriptRegion)); + } } }