From f66fc9fda24e481fb7b53983d95650fa02ff4d98 Mon Sep 17 00:00:00 2001 From: Tyler Leonhardt Date: Tue, 30 Jul 2019 17:34:59 -0700 Subject: [PATCH 1/3] working formatting --- .../LanguageServer/OmnisharpLanguageServer.cs | 4 +- .../Services/Symbols/SymbolsService.cs | 108 ++++++ .../Services/Symbols/Vistors/AstOperations.cs | 335 ++++++++++++++++++ .../Symbols/Vistors/FindCommandVisitor.cs | 87 +++++ .../Symbols/Vistors/FindDeclarationVisitor.cs | 146 ++++++++ .../Symbols/Vistors/FindDotSourcedVisitor.cs | 90 +++++ .../Symbols/Vistors/FindReferencesVisitor.cs | 189 ++++++++++ .../{ => Vistors}/FindSymbolVisitor.cs | 0 .../{ => Vistors}/FindSymbolsVisitor.cs | 0 .../{ => Vistors}/FindSymbolsVisitor2.cs | 0 .../Handlers/DocumentSymbolHandler.cs | 194 ++++++++++ .../Handlers/ReferencesHandler.cs | 189 ++++++++++ .../Services/TextDocument/ScriptFile.cs | 4 +- .../Utility/PathUtils.cs | 32 ++ 14 files changed, 1375 insertions(+), 3 deletions(-) create mode 100644 src/PowerShellEditorServices.Engine/Services/Symbols/Vistors/AstOperations.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/Symbols/Vistors/FindCommandVisitor.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/Symbols/Vistors/FindDeclarationVisitor.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/Symbols/Vistors/FindDotSourcedVisitor.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/Symbols/Vistors/FindReferencesVisitor.cs rename src/PowerShellEditorServices.Engine/Services/Symbols/{ => Vistors}/FindSymbolVisitor.cs (100%) rename src/PowerShellEditorServices.Engine/Services/Symbols/{ => Vistors}/FindSymbolsVisitor.cs (100%) rename src/PowerShellEditorServices.Engine/Services/Symbols/{ => Vistors}/FindSymbolsVisitor2.cs (100%) create mode 100644 src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/DocumentSymbolHandler.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/ReferencesHandler.cs diff --git a/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs b/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs index 73a6d8186..ed92f9083 100644 --- a/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs +++ b/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs @@ -75,7 +75,9 @@ public async Task StartAsync() .WithHandler() .WithHandler() .WithHandler() - .WithHandler(); + .WithHandler() + .WithHandler() + .WithHandler(); }); _serverStart.SetResult(true); diff --git a/src/PowerShellEditorServices.Engine/Services/Symbols/SymbolsService.cs b/src/PowerShellEditorServices.Engine/Services/Symbols/SymbolsService.cs index 1b4a38b19..071742dea 100644 --- a/src/PowerShellEditorServices.Engine/Services/Symbols/SymbolsService.cs +++ b/src/PowerShellEditorServices.Engine/Services/Symbols/SymbolsService.cs @@ -1,4 +1,7 @@ +using System; using System.Collections.Generic; +using System.Collections.Specialized; +using System.Runtime.InteropServices; using Microsoft.Extensions.Logging; using Microsoft.PowerShell.EditorServices.Symbols; @@ -68,5 +71,110 @@ public List FindSymbolsInFile(ScriptFile scriptFile) return foundOccurrences; } + + /// + /// Finds the symbol in the script given a file location + /// + /// The details and contents of a open script file + /// The line number of the cursor for the given script + /// The coulumn number of the cursor for the given script + /// A SymbolReference of the symbol found at the given location + /// or null if there is no symbol at that location + /// + public SymbolReference FindSymbolAtLocation( + ScriptFile scriptFile, + int lineNumber, + int columnNumber) + { + SymbolReference symbolReference = + AstOperations.FindSymbolAtPosition( + scriptFile.ScriptAst, + lineNumber, + columnNumber); + + if (symbolReference != null) + { + symbolReference.FilePath = scriptFile.FilePath; + } + + return symbolReference; + } + + /// + /// Finds all the references of a symbol + /// + /// The symbol to find all references for + /// An array of scriptFiles too search for references in + /// The workspace that will be searched for symbols + /// FindReferencesResult + public List FindReferencesOfSymbol( + SymbolReference foundSymbol, + ScriptFile[] referencedFiles, + WorkspaceService workspace) + { + if (foundSymbol == null) + { + return null; + } + + int symbolOffset = referencedFiles[0].GetOffsetAtPosition( + foundSymbol.ScriptRegion.StartLineNumber, + foundSymbol.ScriptRegion.StartColumnNumber); + + // NOTE: we use to make sure aliases were loaded but took it out because we needed the pipeline thread. + + // We want to look for references first in referenced files, hence we use ordered dictionary + // TODO: File system case-sensitivity is based on filesystem not OS, but OS is a much cheaper heuristic + var fileMap = RuntimeInformation.IsOSPlatform(OSPlatform.Linux) + ? new OrderedDictionary() + : new OrderedDictionary(StringComparer.OrdinalIgnoreCase); + + foreach (ScriptFile scriptFile in referencedFiles) + { + fileMap[scriptFile.FilePath] = scriptFile; + } + + foreach (string filePath in workspace.EnumeratePSFiles()) + { + if (!fileMap.Contains(filePath)) + { + if (!workspace.TryGetFile(filePath, out ScriptFile scriptFile)) + { + // If we can't access the file for some reason, just ignore it + continue; + } + + fileMap[filePath] = scriptFile; + } + } + + var symbolReferences = new List(); + foreach (object fileName in fileMap.Keys) + { + var file = (ScriptFile)fileMap[fileName]; + + IEnumerable references = AstOperations.FindReferencesOfSymbol( + file.ScriptAst, + foundSymbol, + needsAliases: false); + + foreach (SymbolReference reference in references) + { + try + { + reference.SourceLine = file.GetLine(reference.ScriptRegion.StartLineNumber); + } + catch (ArgumentOutOfRangeException e) + { + reference.SourceLine = string.Empty; + _logger.LogException("Found reference is out of range in script file", e); + } + reference.FilePath = file.FilePath; + symbolReferences.Add(reference); + } + } + + return symbolReferences; + } } } diff --git a/src/PowerShellEditorServices.Engine/Services/Symbols/Vistors/AstOperations.cs b/src/PowerShellEditorServices.Engine/Services/Symbols/Vistors/AstOperations.cs new file mode 100644 index 000000000..9e003c0d4 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/Symbols/Vistors/AstOperations.cs @@ -0,0 +1,335 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Management.Automation.Language; + +namespace Microsoft.PowerShell.EditorServices.Symbols +{ + /// + /// Provides common operations for the syntax tree of a parsed script. + /// + internal static class AstOperations + { + // TODO: When netstandard is upgraded to 2.0, see if + // Delegate.CreateDelegate can be used here instead + //private static readonly MethodInfo s_extentCloneWithNewOffset = typeof(PSObject).GetTypeInfo().Assembly + // .GetType("System.Management.Automation.Language.InternalScriptPosition") + // .GetMethod("CloneWithNewOffset", BindingFlags.Instance | BindingFlags.NonPublic); + + //private static readonly SemaphoreSlim s_completionHandle = AsyncUtils.CreateSimpleLockingSemaphore(); + + // TODO: BRING THIS BACK + /// + /// Gets completions for the symbol found in the Ast at + /// the given file offset. + /// + /// + /// The Ast which will be traversed to find a completable symbol. + /// + /// + /// The array of tokens corresponding to the scriptAst parameter. + /// + /// + /// The 1-based file offset at which a symbol will be located. + /// + /// + /// The PowerShellContext to use for gathering completions. + /// + /// An ILogger implementation used for writing log messages. + /// + /// A CancellationToken to cancel completion requests. + /// + /// + /// A CommandCompletion instance that contains completions for the + /// symbol at the given offset. + /// + // static public async Task GetCompletionsAsync( + // Ast scriptAst, + // Token[] currentTokens, + // int fileOffset, + // PowerShellContext powerShellContext, + // ILogger logger, + // CancellationToken cancellationToken) + // { + // if (!s_completionHandle.Wait(0)) + // { + // return null; + // } + + // try + // { + // IScriptPosition cursorPosition = (IScriptPosition)s_extentCloneWithNewOffset.Invoke( + // scriptAst.Extent.StartScriptPosition, + // new object[] { fileOffset }); + + // logger.Write( + // LogLevel.Verbose, + // string.Format( + // "Getting completions at offset {0} (line: {1}, column: {2})", + // fileOffset, + // cursorPosition.LineNumber, + // cursorPosition.ColumnNumber)); + + // if (!powerShellContext.IsAvailable) + // { + // return null; + // } + + // var stopwatch = new Stopwatch(); + + // // If the current runspace is out of process we can use + // // CommandCompletion.CompleteInput because PSReadLine won't be taking up the + // // main runspace. + // if (powerShellContext.IsCurrentRunspaceOutOfProcess()) + // { + // using (RunspaceHandle runspaceHandle = await powerShellContext.GetRunspaceHandleAsync(cancellationToken)) + // using (PowerShell powerShell = PowerShell.Create()) + // { + // powerShell.Runspace = runspaceHandle.Runspace; + // stopwatch.Start(); + // try + // { + // return CommandCompletion.CompleteInput( + // scriptAst, + // currentTokens, + // cursorPosition, + // options: null, + // powershell: powerShell); + // } + // finally + // { + // stopwatch.Stop(); + // logger.Write(LogLevel.Verbose, $"IntelliSense completed in {stopwatch.ElapsedMilliseconds}ms."); + // } + // } + // } + + // CommandCompletion commandCompletion = null; + // await powerShellContext.InvokeOnPipelineThreadAsync( + // pwsh => + // { + // stopwatch.Start(); + // commandCompletion = CommandCompletion.CompleteInput( + // scriptAst, + // currentTokens, + // cursorPosition, + // options: null, + // powershell: pwsh); + // }); + // stopwatch.Stop(); + // logger.Write(LogLevel.Verbose, $"IntelliSense completed in {stopwatch.ElapsedMilliseconds}ms."); + + // return commandCompletion; + // } + // finally + // { + // s_completionHandle.Release(); + // } + // } + + /// + /// Finds the symbol at a given file location + /// + /// The abstract syntax tree of the given script + /// The line number of the cursor for the given script + /// The coulumn number of the cursor for the given script + /// Includes full function definition ranges in the search. + /// SymbolReference of found symbol + static public SymbolReference FindSymbolAtPosition( + Ast scriptAst, + int lineNumber, + int columnNumber, + bool includeFunctionDefinitions = false) + { + FindSymbolVisitor symbolVisitor = + new FindSymbolVisitor( + lineNumber, + columnNumber, + includeFunctionDefinitions); + + scriptAst.Visit(symbolVisitor); + + return symbolVisitor.FoundSymbolReference; + } + + /// + /// Finds the symbol (always Command type) at a given file location + /// + /// The abstract syntax tree of the given script + /// The line number of the cursor for the given script + /// The column number of the cursor for the given script + /// SymbolReference of found command + static public SymbolReference FindCommandAtPosition(Ast scriptAst, int lineNumber, int columnNumber) + { + FindCommandVisitor commandVisitor = new FindCommandVisitor(lineNumber, columnNumber); + scriptAst.Visit(commandVisitor); + + return commandVisitor.FoundCommandReference; + } + + /// + /// Finds all references (including aliases) in a script for the given symbol + /// + /// The abstract syntax tree of the given script + /// The symbol that we are looking for referneces of + /// Dictionary maping cmdlets to aliases for finding alias references + /// Dictionary maping aliases to cmdlets for finding alias references + /// + static public IEnumerable FindReferencesOfSymbol( + Ast scriptAst, + SymbolReference symbolReference, + Dictionary> CmdletToAliasDictionary, + Dictionary AliasToCmdletDictionary) + { + // find the symbol evaluators for the node types we are handling + FindReferencesVisitor referencesVisitor = + new FindReferencesVisitor( + symbolReference, + CmdletToAliasDictionary, + AliasToCmdletDictionary); + scriptAst.Visit(referencesVisitor); + + return referencesVisitor.FoundReferences; + } + + /// + /// Finds all references (not including aliases) in a script for the given symbol + /// + /// The abstract syntax tree of the given script + /// The symbol that we are looking for referneces of + /// If this reference search needs aliases. + /// This should always be false and used for occurence requests + /// A collection of SymbolReference objects that are refrences to the symbolRefrence + /// not including aliases + static public IEnumerable FindReferencesOfSymbol( + ScriptBlockAst scriptAst, + SymbolReference foundSymbol, + bool needsAliases) + { + FindReferencesVisitor referencesVisitor = + new FindReferencesVisitor(foundSymbol); + scriptAst.Visit(referencesVisitor); + + return referencesVisitor.FoundReferences; + } + + /// + /// Finds the definition of the symbol + /// + /// The abstract syntax tree of the given script + /// The symbol that we are looking for the definition of + /// A SymbolReference of the definition of the symbolReference + static public SymbolReference FindDefinitionOfSymbol( + Ast scriptAst, + SymbolReference symbolReference) + { + FindDeclarationVisitor declarationVisitor = + new FindDeclarationVisitor( + symbolReference); + scriptAst.Visit(declarationVisitor); + + return declarationVisitor.FoundDeclaration; + } + + /// + /// Finds all symbols in a script + /// + /// The abstract syntax tree of the given script + /// The PowerShell version the Ast was generated from + /// A collection of SymbolReference objects + static public IEnumerable FindSymbolsInDocument(Ast scriptAst, Version powerShellVersion) + { + IEnumerable symbolReferences = null; + + // TODO: Restore this when we figure out how to support multiple + // PS versions in the new PSES-as-a-module world (issue #276) + // if (powerShellVersion >= new Version(5,0)) + // { + //#if PowerShellv5 + // FindSymbolsVisitor2 findSymbolsVisitor = new FindSymbolsVisitor2(); + // scriptAst.Visit(findSymbolsVisitor); + // symbolReferences = findSymbolsVisitor.SymbolReferences; + //#endif + // } + // else + + FindSymbolsVisitor findSymbolsVisitor = new FindSymbolsVisitor(); + scriptAst.Visit(findSymbolsVisitor); + symbolReferences = findSymbolsVisitor.SymbolReferences; + return symbolReferences; + } + + /// + /// Checks if a given ast represents the root node of a *.psd1 file. + /// + /// The abstract syntax tree of the given script + /// true if the AST represts a *.psd1 file, otherwise false + static public bool IsPowerShellDataFileAst(Ast ast) + { + // sometimes we don't have reliable access to the filename + // so we employ heuristics to check if the contents are + // part of a psd1 file. + return IsPowerShellDataFileAstNode( + new { Item = ast, Children = new List() }, + new Type[] { + typeof(ScriptBlockAst), + typeof(NamedBlockAst), + typeof(PipelineAst), + typeof(CommandExpressionAst), + typeof(HashtableAst) }, + 0); + } + + static private bool IsPowerShellDataFileAstNode(dynamic node, Type[] levelAstMap, int level) + { + var levelAstTypeMatch = node.Item.GetType().Equals(levelAstMap[level]); + if (!levelAstTypeMatch) + { + return false; + } + + if (level == levelAstMap.Length - 1) + { + return levelAstTypeMatch; + } + + var astsFound = (node.Item as Ast).FindAll(a => a is Ast, false); + if (astsFound != null) + { + foreach (var astFound in astsFound) + { + if (!astFound.Equals(node.Item) + && node.Item.Equals(astFound.Parent) + && IsPowerShellDataFileAstNode( + new { Item = astFound, Children = new List() }, + levelAstMap, + level + 1)) + { + return true; + } + } + } + + return false; + } + + /// + /// Finds all files dot sourced in a script + /// + /// The abstract syntax tree of the given script + /// Pre-calculated value of $PSScriptRoot + /// + static public string[] FindDotSourcedIncludes(Ast scriptAst, string psScriptRoot) + { + FindDotSourcedVisitor dotSourcedVisitor = new FindDotSourcedVisitor(psScriptRoot); + scriptAst.Visit(dotSourcedVisitor); + + return dotSourcedVisitor.DotSourcedFiles.ToArray(); + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/Symbols/Vistors/FindCommandVisitor.cs b/src/PowerShellEditorServices.Engine/Services/Symbols/Vistors/FindCommandVisitor.cs new file mode 100644 index 000000000..254bf6ba3 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/Symbols/Vistors/FindCommandVisitor.cs @@ -0,0 +1,87 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Linq; +using System.Management.Automation.Language; + +namespace Microsoft.PowerShell.EditorServices.Symbols +{ + /// + /// The vistior used to find the commandAst of a specific location in an AST + /// + internal class FindCommandVisitor : AstVisitor + { + private readonly int lineNumber; + private readonly int columnNumber; + + public SymbolReference FoundCommandReference { get; private set; } + + public FindCommandVisitor(int lineNumber, int columnNumber) + { + this.lineNumber = lineNumber; + this.columnNumber = columnNumber; + } + + public override AstVisitAction VisitPipeline(PipelineAst pipelineAst) + { + if (this.lineNumber == pipelineAst.Extent.StartLineNumber) + { + // Which command is the cursor in? + foreach (var commandAst in pipelineAst.PipelineElements.OfType()) + { + int trueEndColumnNumber = commandAst.Extent.EndColumnNumber; + string currentLine = commandAst.Extent.StartScriptPosition.Line; + + if (currentLine.Length >= trueEndColumnNumber) + { + // Get the text left in the line after the command's extent + string remainingLine = + currentLine.Substring( + commandAst.Extent.EndColumnNumber); + + // Calculate the "true" end column number by finding out how many + // whitespace characters are between this command and the next (or + // the end of the line). + // NOTE: +1 is added to trueEndColumnNumber to account for the position + // just after the last character in the command string or script line. + int preTrimLength = remainingLine.Length; + int postTrimLength = remainingLine.TrimStart().Length; + trueEndColumnNumber = + commandAst.Extent.EndColumnNumber + + (preTrimLength - postTrimLength) + 1; + } + + if (commandAst.Extent.StartColumnNumber <= columnNumber && + trueEndColumnNumber >= columnNumber) + { + this.FoundCommandReference = + new SymbolReference( + SymbolType.Function, + commandAst.CommandElements[0].Extent); + + return AstVisitAction.StopVisit; + } + } + } + + return base.VisitPipeline(pipelineAst); + } + + /// + /// Is the position of the given location is in the range of the start + /// of the first element to the character before the second element + /// + /// The script extent of the first element of the command ast + /// The script extent of the second element of the command ast + /// True if the given position is in the range of the start of + /// the first element to the character before the second element + private bool IsPositionInExtent(IScriptExtent firstExtent, IScriptExtent secondExtent) + { + return (firstExtent.StartLineNumber == lineNumber && + firstExtent.StartColumnNumber <= columnNumber && + secondExtent.StartColumnNumber >= columnNumber - 1); + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/Symbols/Vistors/FindDeclarationVisitor.cs b/src/PowerShellEditorServices.Engine/Services/Symbols/Vistors/FindDeclarationVisitor.cs new file mode 100644 index 000000000..c9842c9ef --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/Symbols/Vistors/FindDeclarationVisitor.cs @@ -0,0 +1,146 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using System.Management.Automation.Language; + +namespace Microsoft.PowerShell.EditorServices.Symbols +{ + /// + /// The visitor used to find the definition of a symbol + /// + internal class FindDeclarationVisitor : AstVisitor + { + private SymbolReference symbolRef; + private string variableName; + + public SymbolReference FoundDeclaration{ get; private set; } + + public FindDeclarationVisitor(SymbolReference symbolRef) + { + this.symbolRef = symbolRef; + if (this.symbolRef.SymbolType == SymbolType.Variable) + { + // converts `$varName` to `varName` or of the form ${varName} to varName + variableName = symbolRef.SymbolName.TrimStart('$').Trim('{', '}'); + } + } + + /// + /// Decides if the current function definition is the right definition + /// for the symbol being searched for. The definition of the symbol will be a of type + /// SymbolType.Function and have the same name as the symbol + /// + /// A FunctionDefinitionAst in the script's AST + /// A decision to stop searching if the right FunctionDefinitionAst was found, + /// or a decision to continue if it wasn't found + public override AstVisitAction VisitFunctionDefinition(FunctionDefinitionAst functionDefinitionAst) + { + // Get the start column number of the function name, + // instead of the the start column of 'function' and create new extent for the functionName + int startColumnNumber = + functionDefinitionAst.Extent.Text.IndexOf( + functionDefinitionAst.Name, StringComparison.OrdinalIgnoreCase) + 1; + + IScriptExtent nameExtent = new ScriptExtent() + { + Text = functionDefinitionAst.Name, + StartLineNumber = functionDefinitionAst.Extent.StartLineNumber, + StartColumnNumber = startColumnNumber, + EndLineNumber = functionDefinitionAst.Extent.StartLineNumber, + EndColumnNumber = startColumnNumber + functionDefinitionAst.Name.Length + }; + + if (symbolRef.SymbolType.Equals(SymbolType.Function) && + nameExtent.Text.Equals(symbolRef.ScriptRegion.Text, StringComparison.CurrentCultureIgnoreCase)) + { + this.FoundDeclaration = + new SymbolReference( + SymbolType.Function, + nameExtent); + + return AstVisitAction.StopVisit; + } + + return base.VisitFunctionDefinition(functionDefinitionAst); + } + + /// + /// Check if the left hand side of an assignmentStatementAst is a VariableExpressionAst + /// with the same name as that of symbolRef. + /// + /// An AssignmentStatementAst + /// A decision to stop searching if the right VariableExpressionAst was found, + /// or a decision to continue if it wasn't found + public override AstVisitAction VisitAssignmentStatement(AssignmentStatementAst assignmentStatementAst) + { + if (variableName == null) + { + return AstVisitAction.Continue; + } + + // We want to check VariableExpressionAsts from within this AssignmentStatementAst so we visit it. + FindDeclarationVariableExpressionVisitor visitor = new FindDeclarationVariableExpressionVisitor(symbolRef); + assignmentStatementAst.Left.Visit(visitor); + + if (visitor.FoundDeclaration != null) + { + FoundDeclaration = visitor.FoundDeclaration; + return AstVisitAction.StopVisit; + } + return AstVisitAction.Continue; + } + + /// + /// The private visitor used to find the variable expression that matches a symbol + /// + private class FindDeclarationVariableExpressionVisitor : AstVisitor + { + private SymbolReference symbolRef; + private string variableName; + + public SymbolReference FoundDeclaration{ get; private set; } + + public FindDeclarationVariableExpressionVisitor(SymbolReference symbolRef) + { + this.symbolRef = symbolRef; + if (this.symbolRef.SymbolType == SymbolType.Variable) + { + // converts `$varName` to `varName` or of the form ${varName} to varName + variableName = symbolRef.SymbolName.TrimStart('$').Trim('{', '}'); + } + } + + /// + /// Check if the VariableExpressionAst has the same name as that of symbolRef. + /// + /// A VariableExpressionAst + /// A decision to stop searching if the right VariableExpressionAst was found, + /// or a decision to continue if it wasn't found + public override AstVisitAction VisitVariableExpression(VariableExpressionAst variableExpressionAst) + { + if (variableExpressionAst.VariablePath.UserPath.Equals(variableName, StringComparison.OrdinalIgnoreCase)) + { + // TODO also find instances of set-variable + FoundDeclaration = new SymbolReference(SymbolType.Variable, variableExpressionAst.Extent); + return AstVisitAction.StopVisit; + } + return AstVisitAction.Continue; + } + + public override AstVisitAction VisitMemberExpression(MemberExpressionAst functionDefinitionAst) + { + // We don't want to discover any variables in member expressisons (`$something.Foo`) + return AstVisitAction.SkipChildren; + } + + public override AstVisitAction VisitIndexExpression(IndexExpressionAst functionDefinitionAst) + { + // We don't want to discover any variables in index expressions (`$something[0]`) + return AstVisitAction.SkipChildren; + } + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/Symbols/Vistors/FindDotSourcedVisitor.cs b/src/PowerShellEditorServices.Engine/Services/Symbols/Vistors/FindDotSourcedVisitor.cs new file mode 100644 index 000000000..de42c7753 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/Symbols/Vistors/FindDotSourcedVisitor.cs @@ -0,0 +1,90 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using System.Collections.Generic; +using System.Management.Automation.Language; +using PowerShellEditorServices.Engine.Utility; + +namespace Microsoft.PowerShell.EditorServices.Symbols +{ + /// + /// The vistor used to find the dont sourced files in an AST + /// + internal class FindDotSourcedVisitor : AstVisitor + { + private readonly string _psScriptRoot; + + /// + /// A hash set of the dot sourced files (because we don't want duplicates) + /// + public HashSet DotSourcedFiles { get; private set; } + + /// + /// Creates a new instance of the FindDotSourcedVisitor class. + /// + /// Pre-calculated value of $PSScriptRoot + public FindDotSourcedVisitor(string psScriptRoot) + { + DotSourcedFiles = new HashSet(StringComparer.CurrentCultureIgnoreCase); + _psScriptRoot = psScriptRoot; + } + + /// + /// Checks to see if the command invocation is a dot + /// in order to find a dot sourced file + /// + /// A CommandAst object in the script's AST + /// A decision to stop searching if the right commandAst was found, + /// or a decision to continue if it wasn't found + public override AstVisitAction VisitCommand(CommandAst commandAst) + { + CommandElementAst commandElementAst = commandAst.CommandElements[0]; + if (commandAst.InvocationOperator.Equals(TokenKind.Dot)) + { + string path; + switch (commandElementAst) + { + case StringConstantExpressionAst stringConstantExpressionAst: + path = stringConstantExpressionAst.Value; + break; + + case ExpandableStringExpressionAst expandableStringExpressionAst: + path = GetPathFromExpandableStringExpression(expandableStringExpressionAst); + break; + + default: + path = null; + break; + } + + if (!string.IsNullOrWhiteSpace(path)) + { + DotSourcedFiles.Add(PathUtils.NormalizePathSeparators(path)); + } + } + + return base.VisitCommand(commandAst); + } + + private string GetPathFromExpandableStringExpression(ExpandableStringExpressionAst expandableStringExpressionAst) + { + var path = expandableStringExpressionAst.Value; + foreach (var nestedExpression in expandableStringExpressionAst.NestedExpressions) + { + // If the string contains the variable $PSScriptRoot, we replace it with the corresponding value. + if (!(nestedExpression is VariableExpressionAst variableAst + && variableAst.VariablePath.UserPath.Equals("PSScriptRoot", StringComparison.OrdinalIgnoreCase))) + { + return null; // We return null instead of a partially evaluated ExpandableStringExpression. + } + + path = path.Replace(variableAst.ToString(), _psScriptRoot); + } + + return path; + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/Symbols/Vistors/FindReferencesVisitor.cs b/src/PowerShellEditorServices.Engine/Services/Symbols/Vistors/FindReferencesVisitor.cs new file mode 100644 index 000000000..2dfb6d86b --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/Symbols/Vistors/FindReferencesVisitor.cs @@ -0,0 +1,189 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using System.Collections.Generic; +using System.Management.Automation.Language; + +namespace Microsoft.PowerShell.EditorServices.Symbols +{ + /// + /// The visitor used to find the references of a symbol in a script's AST + /// + internal class FindReferencesVisitor : AstVisitor + { + private SymbolReference symbolRef; + private Dictionary> CmdletToAliasDictionary; + private Dictionary AliasToCmdletDictionary; + private string symbolRefCommandName; + private bool needsAliases; + + public List FoundReferences { get; set; } + + /// + /// Constructor used when searching for aliases is needed + /// + /// The found symbolReference that other symbols are being compared to + /// Dictionary maping cmdlets to aliases for finding alias references + /// Dictionary maping aliases to cmdlets for finding alias references + public FindReferencesVisitor( + SymbolReference symbolReference, + Dictionary> CmdletToAliasDictionary, + Dictionary AliasToCmdletDictionary) + { + this.symbolRef = symbolReference; + this.FoundReferences = new List(); + this.needsAliases = true; + this.CmdletToAliasDictionary = CmdletToAliasDictionary; + this.AliasToCmdletDictionary = AliasToCmdletDictionary; + + // Try to get the symbolReference's command name of an alias, + // if a command name does not exists (if the symbol isn't an alias to a command) + // set symbolRefCommandName to and empty string value + AliasToCmdletDictionary.TryGetValue(symbolReference.ScriptRegion.Text, out symbolRefCommandName); + if (symbolRefCommandName == null) { symbolRefCommandName = string.Empty; } + + } + + /// + /// Constructor used when searching for aliases is not needed + /// + /// The found symbolReference that other symbols are being compared to + public FindReferencesVisitor(SymbolReference foundSymbol) + { + this.symbolRef = foundSymbol; + this.FoundReferences = new List(); + this.needsAliases = false; + } + + /// + /// Decides if the current command is a reference of the symbol being searched for. + /// A reference of the symbol will be a of type SymbolType.Function + /// and have the same name as the symbol + /// + /// A CommandAst in the script's AST + /// A visit action that continues the search for references + public override AstVisitAction VisitCommand(CommandAst commandAst) + { + Ast commandNameAst = commandAst.CommandElements[0]; + string commandName = commandNameAst.Extent.Text; + + if(symbolRef.SymbolType.Equals(SymbolType.Function)) + { + if (needsAliases) + { + // Try to get the commandAst's name and aliases, + // if a command does not exists (if the symbol isn't an alias to a command) + // set command to and empty string value string command + // if the aliases do not exist (if the symvol isn't a command that has aliases) + // set aliases to an empty List + string command; + List alaises; + CmdletToAliasDictionary.TryGetValue(commandName, out alaises); + AliasToCmdletDictionary.TryGetValue(commandName, out command); + if (alaises == null) { alaises = new List(); } + if (command == null) { command = string.Empty; } + + if (symbolRef.SymbolType.Equals(SymbolType.Function)) + { + // Check if the found symbol's name is the same as the commandAst's name OR + // if the symbol's name is an alias for this commandAst's name (commandAst is a cmdlet) OR + // if the symbol's name is the same as the commandAst's cmdlet name (commandAst is a alias) + if (commandName.Equals(symbolRef.SymbolName, StringComparison.CurrentCultureIgnoreCase) || + alaises.Contains(symbolRef.ScriptRegion.Text.ToLower()) || + command.Equals(symbolRef.ScriptRegion.Text, StringComparison.CurrentCultureIgnoreCase) || + (!command.Equals(string.Empty) && command.Equals(symbolRefCommandName, StringComparison.CurrentCultureIgnoreCase))) + { + this.FoundReferences.Add(new SymbolReference( + SymbolType.Function, + commandNameAst.Extent)); + } + } + + } + else // search does not include aliases + { + if (commandName.Equals(symbolRef.SymbolName, StringComparison.CurrentCultureIgnoreCase)) + { + this.FoundReferences.Add(new SymbolReference( + SymbolType.Function, + commandNameAst.Extent)); + } + } + + } + return base.VisitCommand(commandAst); + } + + /// + /// Decides if the current function definition is a reference of the symbol being searched for. + /// A reference of the symbol will be a of type SymbolType.Function and have the same name as the symbol + /// + /// A functionDefinitionAst in the script's AST + /// A visit action that continues the search for references + public override AstVisitAction VisitFunctionDefinition(FunctionDefinitionAst functionDefinitionAst) + { + // Get the start column number of the function name, + // instead of the the start column of 'function' and create new extent for the functionName + int startColumnNumber = + functionDefinitionAst.Extent.Text.IndexOf( + functionDefinitionAst.Name) + 1; + + IScriptExtent nameExtent = new ScriptExtent() + { + Text = functionDefinitionAst.Name, + StartLineNumber = functionDefinitionAst.Extent.StartLineNumber, + EndLineNumber = functionDefinitionAst.Extent.StartLineNumber, + StartColumnNumber = startColumnNumber, + EndColumnNumber = startColumnNumber + functionDefinitionAst.Name.Length + }; + + if (symbolRef.SymbolType.Equals(SymbolType.Function) && + nameExtent.Text.Equals(symbolRef.SymbolName, StringComparison.CurrentCultureIgnoreCase)) + { + this.FoundReferences.Add(new SymbolReference( + SymbolType.Function, + nameExtent)); + } + return base.VisitFunctionDefinition(functionDefinitionAst); + } + + /// + /// Decides if the current function definition is a reference of the symbol being searched for. + /// A reference of the symbol will be a of type SymbolType.Parameter and have the same name as the symbol + /// + /// A commandParameterAst in the script's AST + /// A visit action that continues the search for references + public override AstVisitAction VisitCommandParameter(CommandParameterAst commandParameterAst) + { + if (symbolRef.SymbolType.Equals(SymbolType.Parameter) && + commandParameterAst.Extent.Text.Equals(symbolRef.SymbolName, StringComparison.CurrentCultureIgnoreCase)) + { + this.FoundReferences.Add(new SymbolReference( + SymbolType.Parameter, + commandParameterAst.Extent)); + } + return AstVisitAction.Continue; + } + + /// + /// Decides if the current function definition is a reference of the symbol being searched for. + /// A reference of the symbol will be a of type SymbolType.Variable and have the same name as the symbol + /// + /// A variableExpressionAst in the script's AST + /// A visit action that continues the search for references + public override AstVisitAction VisitVariableExpression(VariableExpressionAst variableExpressionAst) + { + if(symbolRef.SymbolType.Equals(SymbolType.Variable) && + variableExpressionAst.Extent.Text.Equals(symbolRef.SymbolName, StringComparison.CurrentCultureIgnoreCase)) + { + this.FoundReferences.Add(new SymbolReference( + SymbolType.Variable, + variableExpressionAst.Extent)); + } + return AstVisitAction.Continue; + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/Symbols/FindSymbolVisitor.cs b/src/PowerShellEditorServices.Engine/Services/Symbols/Vistors/FindSymbolVisitor.cs similarity index 100% rename from src/PowerShellEditorServices.Engine/Services/Symbols/FindSymbolVisitor.cs rename to src/PowerShellEditorServices.Engine/Services/Symbols/Vistors/FindSymbolVisitor.cs diff --git a/src/PowerShellEditorServices.Engine/Services/Symbols/FindSymbolsVisitor.cs b/src/PowerShellEditorServices.Engine/Services/Symbols/Vistors/FindSymbolsVisitor.cs similarity index 100% rename from src/PowerShellEditorServices.Engine/Services/Symbols/FindSymbolsVisitor.cs rename to src/PowerShellEditorServices.Engine/Services/Symbols/Vistors/FindSymbolsVisitor.cs diff --git a/src/PowerShellEditorServices.Engine/Services/Symbols/FindSymbolsVisitor2.cs b/src/PowerShellEditorServices.Engine/Services/Symbols/Vistors/FindSymbolsVisitor2.cs similarity index 100% rename from src/PowerShellEditorServices.Engine/Services/Symbols/FindSymbolsVisitor2.cs rename to src/PowerShellEditorServices.Engine/Services/Symbols/Vistors/FindSymbolsVisitor2.cs diff --git a/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/DocumentSymbolHandler.cs b/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/DocumentSymbolHandler.cs new file mode 100644 index 000000000..e5289746c --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/DocumentSymbolHandler.cs @@ -0,0 +1,194 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.PowerShell.EditorServices; +using Microsoft.PowerShell.EditorServices.Symbols; +using OmniSharp.Extensions.LanguageServer.Protocol.Client.Capabilities; +using OmniSharp.Extensions.LanguageServer.Protocol.Models; +using OmniSharp.Extensions.LanguageServer.Protocol.Server; +using PowerShellEditorServices.Engine.Utility; + +namespace PowerShellEditorServices.Engine.Services.Handlers +{ + public class DocumentSymbolHandler : IDocumentSymbolHandler + { + private readonly DocumentSelector _documentSelector = new DocumentSelector( + new DocumentFilter() + { + Pattern = "**/*.ps*1" + } + ); + + private readonly ILogger _logger; + private readonly WorkspaceService _workspaceService; + + private readonly IDocumentSymbolProvider[] _providers; + + private DocumentSymbolCapability _capability; + + public DocumentSymbolHandler(ILoggerFactory factory, ConfigurationService configurationService, WorkspaceService workspaceService) + { + _logger = factory.CreateLogger(); + _workspaceService = workspaceService; + _providers = new IDocumentSymbolProvider[] + { + new ScriptDocumentSymbolProvider( + VersionUtils.PSVersion), + new PsdDocumentSymbolProvider(), + new PesterDocumentSymbolProvider() + }; + } + + public TextDocumentRegistrationOptions GetRegistrationOptions() + { + return new TextDocumentRegistrationOptions() + { + DocumentSelector = _documentSelector, + }; + } + + public Task Handle(DocumentSymbolParams request, CancellationToken cancellationToken) + { + ScriptFile scriptFile = + _workspaceService.GetFile( + request.TextDocument.Uri.ToString()); + + IEnumerable foundSymbols = + this.ProvideDocumentSymbols(scriptFile); + + SymbolInformationOrDocumentSymbol[] symbols = null; + + string containerName = Path.GetFileNameWithoutExtension(scriptFile.FilePath); + + if (foundSymbols != null) + { + symbols = + foundSymbols + .Select(r => + { + return new SymbolInformationOrDocumentSymbol(new SymbolInformation + { + ContainerName = containerName, + Kind = GetSymbolKind(r.SymbolType), + Location = new Location + { + Uri = PathUtils.ToUri(r.FilePath), + Range = GetRangeFromScriptRegion(r.ScriptRegion) + }, + Name = GetDecoratedSymbolName(r) + }); + }) + .ToArray(); + } + else + { + symbols = new SymbolInformationOrDocumentSymbol[0]; + } + + + return Task.FromResult(new SymbolInformationOrDocumentSymbolContainer(symbols)); + } + + public void SetCapability(DocumentSymbolCapability capability) + { + _capability = capability; + } + + private IEnumerable ProvideDocumentSymbols( + ScriptFile scriptFile) + { + return + this.InvokeProviders(p => p.ProvideDocumentSymbols(scriptFile)) + .SelectMany(r => r); + } + + /// + /// Invokes the given function synchronously against all + /// registered providers. + /// + /// The function to be invoked. + /// + /// An IEnumerable containing the results of all providers + /// that were invoked successfully. + /// + protected IEnumerable InvokeProviders( + Func invokeFunc) + { + Stopwatch invokeTimer = new Stopwatch(); + List providerResults = new List(); + + foreach (var provider in this._providers) + { + try + { + invokeTimer.Restart(); + + providerResults.Add(invokeFunc(provider)); + + invokeTimer.Stop(); + + this._logger.LogTrace( + $"Invocation of provider '{provider.GetType().Name}' completed in {invokeTimer.ElapsedMilliseconds}ms."); + } + catch (Exception e) + { + this._logger.LogException( + $"Exception caught while invoking provider {provider.GetType().Name}:", + e); + } + } + + return providerResults; + } + + private static SymbolKind GetSymbolKind(SymbolType symbolType) + { + switch (symbolType) + { + case SymbolType.Configuration: + case SymbolType.Function: + case SymbolType.Workflow: + return SymbolKind.Function; + + default: + return SymbolKind.Variable; + } + } + + private static string GetDecoratedSymbolName(SymbolReference symbolReference) + { + string name = symbolReference.SymbolName; + + if (symbolReference.SymbolType == SymbolType.Configuration || + symbolReference.SymbolType == SymbolType.Function || + symbolReference.SymbolType == SymbolType.Workflow) + { + name += " { }"; + } + + return name; + } + + private static Range GetRangeFromScriptRegion(ScriptRegion scriptRegion) + { + return new Range + { + Start = new Position + { + Line = scriptRegion.StartLineNumber - 1, + Character = scriptRegion.StartColumnNumber - 1 + }, + End = new Position + { + Line = scriptRegion.EndLineNumber - 1, + Character = scriptRegion.EndColumnNumber - 1 + } + }; + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/ReferencesHandler.cs b/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/ReferencesHandler.cs new file mode 100644 index 000000000..1b2cba1f8 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/ReferencesHandler.cs @@ -0,0 +1,189 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.PowerShell.EditorServices; +using Microsoft.PowerShell.EditorServices.Symbols; +using OmniSharp.Extensions.LanguageServer.Protocol.Client.Capabilities; +using OmniSharp.Extensions.LanguageServer.Protocol.Models; +using OmniSharp.Extensions.LanguageServer.Protocol.Server; +using PowerShellEditorServices.Engine.Utility; + +namespace PowerShellEditorServices.Engine.Services.Handlers +{ + class ReferencesHandler : IReferencesHandler + { + private readonly DocumentSelector _documentSelector = new DocumentSelector( + new DocumentFilter() + { + Pattern = "**/*.ps*1" + } + ); + + private readonly ILogger _logger; + private readonly SymbolsService _symbolsService; + private readonly WorkspaceService _workspaceService; + private ReferencesCapability _capability; + + public ReferencesHandler(ILoggerFactory factory, SymbolsService symbolsService, WorkspaceService workspaceService) + { + _logger = factory.CreateLogger(); + _symbolsService = symbolsService; + _workspaceService = workspaceService; + } + + public TextDocumentRegistrationOptions GetRegistrationOptions() + { + return new TextDocumentRegistrationOptions + { + DocumentSelector = _documentSelector + }; + } + + public async Task Handle(ReferenceParams request, CancellationToken cancellationToken) + { + ScriptFile scriptFile = + _workspaceService.GetFile( + request.TextDocument.Uri.ToString()); + + //FindSymbolVisitor symbolVisitor = + // new FindSymbolVisitor( + // (int)request.Position.Line + 1, + // (int)request.Position.Character + 1, + // includeFunctionDefinitions: false); + + //scriptFile.ScriptAst.Visit(symbolVisitor); + + //SymbolReference symbolReference = symbolVisitor.FoundSymbolReference; + + //if (symbolReference == null) + //{ + // return null; + //} + //symbolReference.FilePath = scriptFile.FilePath; + + //int symbolOffset = _workspaceService.ExpandScriptReferences(scriptFile)[0].GetOffsetAtPosition( + // symbolReference.ScriptRegion.StartLineNumber, + // symbolReference.ScriptRegion.StartColumnNumber); + + //// Make sure aliases have been loaded + + //// We want to look for references first in referenced files, hence we use ordered dictionary + //// TODO: File system case-sensitivity is based on filesystem not OS, but OS is a much cheaper heuristic + //var fileMap = RuntimeInformation.IsOSPlatform(OSPlatform.Linux) + // ? new OrderedDictionary() + // : new OrderedDictionary(StringComparer.OrdinalIgnoreCase); + + //ScriptFile[] referencedFiles = _workspaceService.ExpandScriptReferences(scriptFile); + + //foreach (ScriptFile sf in referencedFiles) + //{ + // fileMap[scriptFile.FilePath] = sf; + //} + + //foreach (string filePath in _workspaceService.EnumeratePSFiles()) + //{ + // if (!fileMap.Contains(filePath)) + // { + // if (!_workspaceService.TryGetFile(filePath, out ScriptFile sf)) + // { + // // If we can't access the file for some reason, just ignore it + // continue; + // } + + // fileMap[filePath] = sf; + // } + //} + + //var symbolReferences = new List(); + + //foreach (object fileName in fileMap.Keys) + //{ + // var file = (ScriptFile)fileMap[fileName]; + + // FindReferencesVisitor referencesVisitor = + // new FindReferencesVisitor(symbolReference); + // file.ScriptAst.Visit(referencesVisitor); + + // IEnumerable references = referencesVisitor.FoundReferences; + + // foreach (SymbolReference reference in references) + // { + // try + // { + // reference.SourceLine = file.GetLine(reference.ScriptRegion.StartLineNumber); + // } + // catch (ArgumentOutOfRangeException e) + // { + // reference.SourceLine = string.Empty; + // _logger.LogException("Found reference is out of range in script file", e); + // } + // reference.FilePath = file.FilePath; + // symbolReferences.Add(reference); + // } + + //} + + //var locations = new List(); + + //foreach (SymbolReference foundReference in symbolReferences) + //{ + // locations.Add(new Location + // { + // Uri = PathUtils.ToUri(foundReference.FilePath), + // Range = GetRangeFromScriptRegion(foundReference.ScriptRegion) + // }); + //} + + SymbolReference foundSymbol = + _symbolsService.FindSymbolAtLocation( + scriptFile, + (int)request.Position.Line + 1, + (int)request.Position.Character + 1); + + List referencesResult = + _symbolsService.FindReferencesOfSymbol( + foundSymbol, + _workspaceService.ExpandScriptReferences(scriptFile), + _workspaceService); + + var locations = new List(); + + if (referencesResult != null) + { + foreach (SymbolReference foundReference in referencesResult) + { + locations.Add(new Location + { + Uri = PathUtils.ToUri(foundReference.FilePath), + Range = GetRangeFromScriptRegion(foundReference.ScriptRegion) + }); + } + } + + return new LocationContainer(locations); + } + + public void SetCapability(ReferencesCapability capability) + { + _capability = capability; + } + + private static Range GetRangeFromScriptRegion(ScriptRegion scriptRegion) + { + return new Range + { + Start = new Position + { + Line = scriptRegion.StartLineNumber - 1, + Character = scriptRegion.StartColumnNumber - 1 + }, + End = new Position + { + Line = scriptRegion.EndLineNumber - 1, + Character = scriptRegion.EndColumnNumber - 1 + } + }; + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/TextDocument/ScriptFile.cs b/src/PowerShellEditorServices.Engine/Services/TextDocument/ScriptFile.cs index 9253b81ad..57f80c9c3 100644 --- a/src/PowerShellEditorServices.Engine/Services/TextDocument/ScriptFile.cs +++ b/src/PowerShellEditorServices.Engine/Services/TextDocument/ScriptFile.cs @@ -9,7 +9,7 @@ using System.Linq; using System.Management.Automation; using System.Management.Automation.Language; -using System.Runtime.InteropServices; +using Microsoft.PowerShell.EditorServices.Symbols; namespace Microsoft.PowerShell.EditorServices { @@ -670,7 +670,7 @@ private void ParseFileContents() } // Get all dot sourced referenced files and store them - //this.ReferencedFiles = AstOperations.FindDotSourcedIncludes(this.ScriptAst, Path.GetDirectoryName(this.FilePath)); + this.ReferencedFiles = AstOperations.FindDotSourcedIncludes(this.ScriptAst, Path.GetDirectoryName(this.FilePath)); } #endregion diff --git a/src/PowerShellEditorServices.Engine/Utility/PathUtils.cs b/src/PowerShellEditorServices.Engine/Utility/PathUtils.cs index da9dfa3ad..17d43f77b 100644 --- a/src/PowerShellEditorServices.Engine/Utility/PathUtils.cs +++ b/src/PowerShellEditorServices.Engine/Utility/PathUtils.cs @@ -1,9 +1,31 @@ using System; +using System.IO; +using System.Runtime.InteropServices; namespace PowerShellEditorServices.Engine.Utility { internal class PathUtils { + /// + /// The default path separator used by the base implementation of the providers. + /// + /// Porting note: IO.Path.DirectorySeparatorChar is correct for all platforms. On Windows, + /// it is '\', and on Linux, it is '/', as expected. + /// + internal static readonly char DefaultPathSeparator = Path.DirectorySeparatorChar; + internal static readonly string DefaultPathSeparatorString = DefaultPathSeparator.ToString(); + + /// + /// The alternate path separator used by the base implementation of the providers. + /// + /// Porting note: we do not use .NET's AlternatePathSeparatorChar here because it correctly + /// states that both the default and alternate are '/' on Linux. However, for PowerShell to + /// be "slash agnostic", we need to use the assumption that a '\' is the alternate path + /// separator on Linux. + /// + internal static readonly char AlternatePathSeparator = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? '/' : '\\'; + internal static readonly string AlternatePathSeparatorString = AlternatePathSeparator.ToString(); + public string WildcardUnescapePath(string path) { throw new NotImplementedException(); @@ -29,5 +51,15 @@ public static string FromUri(Uri uri) } return uri.LocalPath; } + + /// + /// Converts all alternate path separators to the current platform's main path separators. + /// + /// The path to normalize. + /// The normalized path. + public static string NormalizePathSeparators(string path) + { + return string.IsNullOrWhiteSpace(path) ? path : path.Replace(AlternatePathSeparator, DefaultPathSeparator); + } } } From 884bcb5f75be7d6b34b202649b9101dc4abc6f9e Mon Sep 17 00:00:00 2001 From: Tyler Leonhardt Date: Tue, 30 Jul 2019 18:03:19 -0700 Subject: [PATCH 2/3] add tests --- .../EditorServices.Integration.Tests.ps1 | 47 ++++++++++++++ tools/PsesPsClient/PsesPsClient.psd1 | 2 + tools/PsesPsClient/PsesPsClient.psm1 | 61 +++++++++++++++++++ 3 files changed, 110 insertions(+) diff --git a/test/Pester/EditorServices.Integration.Tests.ps1 b/test/Pester/EditorServices.Integration.Tests.ps1 index 2d29d381b..b363d4ccb 100644 --- a/test/Pester/EditorServices.Integration.Tests.ps1 +++ b/test/Pester/EditorServices.Integration.Tests.ps1 @@ -252,6 +252,53 @@ Get-Process $response.Result.newText.Contains("`t") | Should -BeTrue -Because "We expect a tab." } + It "Can handle a textDocument/documentSymbol request" { + $filePath = New-TestFile -Script ' +function Get-Foo { + +} + +Get-Foo +' + + $request = Send-LspDocumentSymbolRequest -Client $client ` + -Uri ([Uri]::new($filePath).AbsoluteUri) + + $response = Get-LspResponse -Client $client -Id $request.Id + + $response.Result.location.range.start.line | Should -BeExactly 1 + $response.Result.location.range.start.character | Should -BeExactly 0 + $response.Result.location.range.end.line | Should -BeExactly 3 + $response.Result.location.range.end.character | Should -BeExactly 1 + } + + It "Can handle a textDocument/references request" { + $filePath = New-TestFile -Script ' +function Get-Bar { + +} + +Get-Bar +' + + $request = Send-LspReferencesRequest -Client $client ` + -Uri ([Uri]::new($filePath).AbsoluteUri) ` + -LineNumber 5 ` + -CharacterNumber 0 + + $response = Get-LspResponse -Client $client -Id $request.Id + + $response.Result.Count | Should -BeExactly 2 + $response.Result[0].range.start.line | Should -BeExactly 1 + $response.Result[0].range.start.character | Should -BeExactly 9 + $response.Result[0].range.end.line | Should -BeExactly 1 + $response.Result[0].range.end.character | Should -BeExactly 16 + $response.Result[1].range.start.line | Should -BeExactly 5 + $response.Result[1].range.start.character | Should -BeExactly 0 + $response.Result[1].range.end.line | Should -BeExactly 5 + $response.Result[1].range.end.character | Should -BeExactly 7 + } + # This test MUST be last It "Shuts down the process properly" { $request = Send-LspShutdownRequest -Client $client diff --git a/tools/PsesPsClient/PsesPsClient.psd1 b/tools/PsesPsClient/PsesPsClient.psd1 index 1bba35a5d..7010f9a3e 100644 --- a/tools/PsesPsClient/PsesPsClient.psd1 +++ b/tools/PsesPsClient/PsesPsClient.psd1 @@ -78,6 +78,8 @@ FunctionsToExport = @( 'Send-LspDidChangeConfigurationRequest', 'Send-LspFormattingRequest', 'Send-LspRangeFormattingRequest', + 'Send-LspDocumentSymbolRequest', + 'Send-LspReferencesRequest', 'Send-LspShutdownRequest', 'Get-LspNotification', 'Get-LspResponse' diff --git a/tools/PsesPsClient/PsesPsClient.psm1 b/tools/PsesPsClient/PsesPsClient.psm1 index bc825a068..bbfec42a4 100644 --- a/tools/PsesPsClient/PsesPsClient.psm1 +++ b/tools/PsesPsClient/PsesPsClient.psm1 @@ -392,6 +392,67 @@ function Send-LspRangeFormattingRequest return Send-LspRequest -Client $Client -Method 'textDocument/rangeFormatting' -Parameters $params } +function Send-LspDocumentSymbolRequest +{ + [OutputType([PsesPsClient.LspRequest])] + param( + [Parameter(Position = 0, Mandatory)] + [PsesPsClient.PsesLspClient] + $Client, + + [Parameter(Mandatory)] + [string] + $Uri + ) + + $params = [Microsoft.PowerShell.EditorServices.Protocol.LanguageServer.DocumentSymbolParams]@{ + TextDocument = [Microsoft.PowerShell.EditorServices.Protocol.LanguageServer.TextDocumentIdentifier]@{ + Uri = $Uri + } + } + return Send-LspRequest -Client $Client -Method 'textDocument/documentSymbol' -Parameters $params +} + +function Send-LspReferencesRequest +{ + [OutputType([PsesPsClient.LspRequest])] + param( + [Parameter(Position = 0, Mandatory)] + [PsesPsClient.PsesLspClient] + $Client, + + [Parameter(Mandatory)] + [string] + $Uri, + + [Parameter(Mandatory)] + [int] + $LineNumber, + + [Parameter(Mandatory)] + [int] + $CharacterNumber, + + [Parameter()] + [switch] + $IncludeDeclaration + ) + + $params = [Microsoft.PowerShell.EditorServices.Protocol.LanguageServer.ReferencesParams]@{ + TextDocument = [Microsoft.PowerShell.EditorServices.Protocol.LanguageServer.TextDocumentIdentifier]@{ + Uri = $Uri + } + Position = [Microsoft.PowerShell.EditorServices.Protocol.LanguageServer.Position]@{ + Line = $LineNumber + Character = $CharacterNumber + } + Context = [Microsoft.PowerShell.EditorServices.Protocol.LanguageServer.ReferencesContext]@{ + IncludeDeclaration = $IncludeDeclaration + } + } + return Send-LspRequest -Client $Client -Method 'textDocument/references' -Parameters $params +} + function Send-LspShutdownRequest { [OutputType([PsesPsClient.LspRequest])] From 94b4dcb25b5979257e4730d4ee917250e1c17a0e Mon Sep 17 00:00:00 2001 From: Tyler Leonhardt Date: Tue, 30 Jul 2019 18:23:30 -0700 Subject: [PATCH 3/3] delete commented out code --- .../Handlers/ReferencesHandler.cs | 89 ------------------- 1 file changed, 89 deletions(-) diff --git a/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/ReferencesHandler.cs b/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/ReferencesHandler.cs index 1b2cba1f8..6e10d5fe8 100644 --- a/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/ReferencesHandler.cs +++ b/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/ReferencesHandler.cs @@ -46,95 +46,6 @@ public async Task Handle(ReferenceParams request, Cancellatio _workspaceService.GetFile( request.TextDocument.Uri.ToString()); - //FindSymbolVisitor symbolVisitor = - // new FindSymbolVisitor( - // (int)request.Position.Line + 1, - // (int)request.Position.Character + 1, - // includeFunctionDefinitions: false); - - //scriptFile.ScriptAst.Visit(symbolVisitor); - - //SymbolReference symbolReference = symbolVisitor.FoundSymbolReference; - - //if (symbolReference == null) - //{ - // return null; - //} - //symbolReference.FilePath = scriptFile.FilePath; - - //int symbolOffset = _workspaceService.ExpandScriptReferences(scriptFile)[0].GetOffsetAtPosition( - // symbolReference.ScriptRegion.StartLineNumber, - // symbolReference.ScriptRegion.StartColumnNumber); - - //// Make sure aliases have been loaded - - //// We want to look for references first in referenced files, hence we use ordered dictionary - //// TODO: File system case-sensitivity is based on filesystem not OS, but OS is a much cheaper heuristic - //var fileMap = RuntimeInformation.IsOSPlatform(OSPlatform.Linux) - // ? new OrderedDictionary() - // : new OrderedDictionary(StringComparer.OrdinalIgnoreCase); - - //ScriptFile[] referencedFiles = _workspaceService.ExpandScriptReferences(scriptFile); - - //foreach (ScriptFile sf in referencedFiles) - //{ - // fileMap[scriptFile.FilePath] = sf; - //} - - //foreach (string filePath in _workspaceService.EnumeratePSFiles()) - //{ - // if (!fileMap.Contains(filePath)) - // { - // if (!_workspaceService.TryGetFile(filePath, out ScriptFile sf)) - // { - // // If we can't access the file for some reason, just ignore it - // continue; - // } - - // fileMap[filePath] = sf; - // } - //} - - //var symbolReferences = new List(); - - //foreach (object fileName in fileMap.Keys) - //{ - // var file = (ScriptFile)fileMap[fileName]; - - // FindReferencesVisitor referencesVisitor = - // new FindReferencesVisitor(symbolReference); - // file.ScriptAst.Visit(referencesVisitor); - - // IEnumerable references = referencesVisitor.FoundReferences; - - // foreach (SymbolReference reference in references) - // { - // try - // { - // reference.SourceLine = file.GetLine(reference.ScriptRegion.StartLineNumber); - // } - // catch (ArgumentOutOfRangeException e) - // { - // reference.SourceLine = string.Empty; - // _logger.LogException("Found reference is out of range in script file", e); - // } - // reference.FilePath = file.FilePath; - // symbolReferences.Add(reference); - // } - - //} - - //var locations = new List(); - - //foreach (SymbolReference foundReference in symbolReferences) - //{ - // locations.Add(new Location - // { - // Uri = PathUtils.ToUri(foundReference.FilePath), - // Range = GetRangeFromScriptRegion(foundReference.ScriptRegion) - // }); - //} - SymbolReference foundSymbol = _symbolsService.FindSymbolAtLocation( scriptFile,