From 18a81d6a4c489e99691471018f4c6cb29bb391d9 Mon Sep 17 00:00:00 2001 From: Tyler Leonhardt Date: Mon, 8 Jul 2019 17:39:19 -0700 Subject: [PATCH] A working WorkspaceSymbolsHandler --- .../Hosting/EditorServicesHost.cs | 7 +- .../LanguageServer/OmnisharpLanguageServer.cs | 2 +- .../Services/Symbols/FindSymbolVisitor.cs | 145 ++++++++++++ .../Services/Symbols/FindSymbolsVisitor.cs | 148 ++++++++++++ .../Services/Symbols/FindSymbolsVisitor2.cs | 80 +++++++ .../Symbols/IDocumentSymbolProvider.cs | 24 ++ .../Services/Symbols/IDocumentSymbols.cs | 32 +++ .../Symbols/PesterDocumentSymbolProvider.cs | 223 ++++++++++++++++++ .../Symbols/PsdDocumentSymbolProvider.cs | 88 +++++++ .../Symbols/ScriptDocumentSymbolProvider.cs | 76 ++++++ .../Services/Symbols/ScriptExtent.cs | 109 +++++++++ .../Services/Symbols/SymbolReference.cs | 89 +++++++ .../Services/Symbols/SymbolType.cs | 48 ++++ .../Services/Symbols/SymbolsService.cs | 72 ++++++ .../Services/TextDocument/ScriptFile.cs | 4 +- .../Handlers/WorkspaceSymbolsHandler.cs | 161 ++++++------- .../WorkspaceFileSystemWrapper.cs | 0 .../WorkspaceService.cs} | 45 ++-- .../Utility/DotNetFacade.cs | 16 -- .../Utility/PathUtils.cs | 21 ++ .../Utility/VersionUtils.cs | 50 ++++ 21 files changed, 1316 insertions(+), 124 deletions(-) create mode 100644 src/PowerShellEditorServices.Engine/Services/Symbols/FindSymbolVisitor.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/Symbols/FindSymbolsVisitor.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/Symbols/FindSymbolsVisitor2.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/Symbols/IDocumentSymbolProvider.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/Symbols/IDocumentSymbols.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/Symbols/PesterDocumentSymbolProvider.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/Symbols/PsdDocumentSymbolProvider.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/Symbols/ScriptDocumentSymbolProvider.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/Symbols/ScriptExtent.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/Symbols/SymbolReference.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/Symbols/SymbolType.cs create mode 100644 src/PowerShellEditorServices.Engine/Services/Symbols/SymbolsService.cs rename src/PowerShellEditorServices.Engine/Services/{TextDocument => Workspace}/WorkspaceFileSystemWrapper.cs (100%) rename src/PowerShellEditorServices.Engine/Services/{TextDocument/Workspace.cs => Workspace/WorkspaceService.cs} (94%) delete mode 100644 src/PowerShellEditorServices.Engine/Utility/DotNetFacade.cs create mode 100644 src/PowerShellEditorServices.Engine/Utility/VersionUtils.cs diff --git a/src/PowerShellEditorServices.Engine/Hosting/EditorServicesHost.cs b/src/PowerShellEditorServices.Engine/Hosting/EditorServicesHost.cs index 19fb247f3..13ba05f78 100644 --- a/src/PowerShellEditorServices.Engine/Hosting/EditorServicesHost.cs +++ b/src/PowerShellEditorServices.Engine/Hosting/EditorServicesHost.cs @@ -219,12 +219,15 @@ public void StartLanguageService( { while (System.Diagnostics.Debugger.IsAttached) { - Console.WriteLine($"{System.Diagnostics.Process.GetCurrentProcess().Id}"); - System.Threading.Thread.Sleep(2000); + Console.WriteLine($"{Process.GetCurrentProcess().Id}"); + Thread.Sleep(2000); } _logger.LogInformation($"LSP NamedPipe: {config.InOutPipeName}\nLSP OutPipe: {config.OutPipeName}"); + _serviceCollection.AddSingleton(); + _serviceCollection.AddSingleton(); + _languageServer = new OmnisharpLanguageServerBuilder(_serviceCollection) { NamedPipeName = config.InOutPipeName ?? config.InPipeName, diff --git a/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs b/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs index 171eb1978..3b893746b 100644 --- a/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs +++ b/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs @@ -86,7 +86,7 @@ private static NamedPipeServerStream CreateNamedPipe( out NamedPipeServerStream outPipe) { // .NET Core implementation is simplest so try that first - if (DotNetFacade.IsNetCore) + if (VersionUtils.IsNetCore) { outPipe = outPipeName == null ? null diff --git a/src/PowerShellEditorServices.Engine/Services/Symbols/FindSymbolVisitor.cs b/src/PowerShellEditorServices.Engine/Services/Symbols/FindSymbolVisitor.cs new file mode 100644 index 000000000..06bdf8235 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/Symbols/FindSymbolVisitor.cs @@ -0,0 +1,145 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Management.Automation.Language; + +namespace Microsoft.PowerShell.EditorServices.Symbols +{ + /// + /// The visitor used to find the the symbol at a specfic location in the AST + /// + internal class FindSymbolVisitor : AstVisitor + { + private int lineNumber; + private int columnNumber; + private bool includeFunctionDefinitions; + + public SymbolReference FoundSymbolReference { get; private set; } + + public FindSymbolVisitor( + int lineNumber, + int columnNumber, + bool includeFunctionDefinitions) + { + this.lineNumber = lineNumber; + this.columnNumber = columnNumber; + this.includeFunctionDefinitions = includeFunctionDefinitions; + } + + /// + /// Checks to see if this command ast is the symbol we are looking for. + /// + /// A CommandAst 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 VisitCommand(CommandAst commandAst) + { + Ast commandNameAst = commandAst.CommandElements[0]; + + if (this.IsPositionInExtent(commandNameAst.Extent)) + { + this.FoundSymbolReference = + new SymbolReference( + SymbolType.Function, + commandNameAst.Extent); + + return AstVisitAction.StopVisit; + } + + return base.VisitCommand(commandAst); + } + + /// + /// Checks to see if this function definition is the symbol we are looking for. + /// + /// 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) + { + int startColumnNumber = 1; + + if (!this.includeFunctionDefinitions) + { + startColumnNumber = + functionDefinitionAst.Extent.Text.IndexOf( + functionDefinitionAst.Name) + 1; + } + + IScriptExtent nameExtent = new ScriptExtent() + { + Text = functionDefinitionAst.Name, + StartLineNumber = functionDefinitionAst.Extent.StartLineNumber, + EndLineNumber = functionDefinitionAst.Extent.EndLineNumber, + StartColumnNumber = startColumnNumber, + EndColumnNumber = startColumnNumber + functionDefinitionAst.Name.Length + }; + + if (this.IsPositionInExtent(nameExtent)) + { + this.FoundSymbolReference = + new SymbolReference( + SymbolType.Function, + nameExtent); + + return AstVisitAction.StopVisit; + } + + return base.VisitFunctionDefinition(functionDefinitionAst); + } + + /// + /// Checks to see if this command parameter is the symbol we are looking for. + /// + /// A CommandParameterAst 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 VisitCommandParameter(CommandParameterAst commandParameterAst) + { + if (this.IsPositionInExtent(commandParameterAst.Extent)) + { + this.FoundSymbolReference = + new SymbolReference( + SymbolType.Parameter, + commandParameterAst.Extent); + return AstVisitAction.StopVisit; + } + 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 decision 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 (this.IsPositionInExtent(variableExpressionAst.Extent)) + { + this.FoundSymbolReference = + new SymbolReference( + SymbolType.Variable, + variableExpressionAst.Extent); + + return AstVisitAction.StopVisit; + } + + return AstVisitAction.Continue; + } + + /// + /// Is the position of the given location is in the ast's extent + /// + /// The script extent of the element + /// True if the given position is in the range of the element's extent + private bool IsPositionInExtent(IScriptExtent extent) + { + return (extent.StartLineNumber == lineNumber && + extent.StartColumnNumber <= columnNumber && + extent.EndColumnNumber >= columnNumber); + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/Symbols/FindSymbolsVisitor.cs b/src/PowerShellEditorServices.Engine/Services/Symbols/FindSymbolsVisitor.cs new file mode 100644 index 000000000..fd624c429 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/Symbols/FindSymbolsVisitor.cs @@ -0,0 +1,148 @@ +// +// 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.Symbols +{ + /// + /// The visitor used to find all the symbols (function and class defs) in the AST. + /// + /// + /// Requires PowerShell v3 or higher + /// + internal class FindSymbolsVisitor : AstVisitor + { + public List SymbolReferences { get; private set; } + + public FindSymbolsVisitor() + { + this.SymbolReferences = new List(); + } + + /// + /// Adds each function definition 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 decision 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; + } + + 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; + } + } + + /// + /// Visitor to find all the keys in Hashtable AST + /// + internal class FindHashtableSymbolsVisitor : AstVisitor + { + /// + /// List of symbols (keys) found in the hashtable + /// + public List SymbolReferences { get; private set; } + + /// + /// Initializes a new instance of FindHashtableSymbolsVisitor class + /// + public FindHashtableSymbolsVisitor() + { + SymbolReferences = new List(); + } + + /// + /// Adds keys in the input hashtable to the symbol reference + /// + public override AstVisitAction VisitHashtable(HashtableAst hashtableAst) + { + if (hashtableAst.KeyValuePairs == null) + { + return AstVisitAction.Continue; + } + + foreach (var kvp in hashtableAst.KeyValuePairs) + { + var keyStrConstExprAst = kvp.Item1 as StringConstantExpressionAst; + if (keyStrConstExprAst != null) + { + IScriptExtent nameExtent = new ScriptExtent() + { + Text = keyStrConstExprAst.Value, + StartLineNumber = kvp.Item1.Extent.StartLineNumber, + EndLineNumber = kvp.Item2.Extent.EndLineNumber, + StartColumnNumber = kvp.Item1.Extent.StartColumnNumber, + EndColumnNumber = kvp.Item2.Extent.EndColumnNumber + }; + + SymbolType symbolType = SymbolType.HashtableKey; + + this.SymbolReferences.Add( + new SymbolReference( + symbolType, + nameExtent)); + + } + } + + return AstVisitAction.Continue; + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/Symbols/FindSymbolsVisitor2.cs b/src/PowerShellEditorServices.Engine/Services/Symbols/FindSymbolsVisitor2.cs new file mode 100644 index 000000000..03628ee3e --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/Symbols/FindSymbolsVisitor2.cs @@ -0,0 +1,80 @@ +// +// 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 +{ + // TODO: Restore this when we figure out how to support multiple + // PS versions in the new PSES-as-a-module world (issue #276) + + ///// + ///// The visitor used to find all the symbols (function and class defs) in the AST. + ///// + ///// + ///// Requires PowerShell v5 or higher + ///// + ///// + //internal class FindSymbolsVisitor2 : AstVisitor2 + //{ + // private FindSymbolsVisitor findSymbolsVisitor; + + // public List SymbolReferences + // { + // get + // { + // return this.findSymbolsVisitor.SymbolReferences; + // } + // } + + // public FindSymbolsVisitor2() + // { + // this.findSymbolsVisitor = new FindSymbolsVisitor(); + // } + + // /// + // /// Adds each function definition 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) + // { + // return this.findSymbolsVisitor.VisitFunctionDefinition(functionDefinitionAst); + // } + + // /// + // /// Checks to see if this variable expression is the symbol we are looking for. + // /// + // /// A VariableExpressionAst 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 VisitVariableExpression(VariableExpressionAst variableExpressionAst) + // { + // return this.findSymbolsVisitor.VisitVariableExpression(variableExpressionAst); + // } + + // 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.findSymbolsVisitor.SymbolReferences.Add( + // new SymbolReference( + // SymbolType.Configuration, + // nameExtent)); + + // return AstVisitAction.Continue; + // } + //} +} + diff --git a/src/PowerShellEditorServices.Engine/Services/Symbols/IDocumentSymbolProvider.cs b/src/PowerShellEditorServices.Engine/Services/Symbols/IDocumentSymbolProvider.cs new file mode 100644 index 000000000..d971b9b38 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/Symbols/IDocumentSymbolProvider.cs @@ -0,0 +1,24 @@ +// +// 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; + +namespace Microsoft.PowerShell.EditorServices.Symbols +{ + /// + /// Specifies the contract for a document symbols provider. + /// + public interface IDocumentSymbolProvider + { + /// + /// Provides a list of symbols for the given document. + /// + /// + /// The document for which SymbolReferences should be provided. + /// + /// An IEnumerable collection of SymbolReferences. + IEnumerable ProvideDocumentSymbols(ScriptFile scriptFile); + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/Symbols/IDocumentSymbols.cs b/src/PowerShellEditorServices.Engine/Services/Symbols/IDocumentSymbols.cs new file mode 100644 index 000000000..42472203e --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/Symbols/IDocumentSymbols.cs @@ -0,0 +1,32 @@ +// +// 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.Collections.ObjectModel; + +namespace Microsoft.PowerShell.EditorServices.Symbols +{ + /// + /// Specifies the contract for an implementation of + /// the IDocumentSymbols component. + /// + public interface IDocumentSymbols + { + /// + /// Gets the collection of IDocumentSymbolsProvider implementations + /// that are registered with this component. + /// + Collection Providers { get; } + + /// + /// Provides a list of symbols for the given document. + /// + /// + /// The document for which SymbolReferences should be provided. + /// + /// An IEnumerable collection of SymbolReferences. + IEnumerable ProvideDocumentSymbols(ScriptFile scriptFile); + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/Symbols/PesterDocumentSymbolProvider.cs b/src/PowerShellEditorServices.Engine/Services/Symbols/PesterDocumentSymbolProvider.cs new file mode 100644 index 000000000..87a19778e --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/Symbols/PesterDocumentSymbolProvider.cs @@ -0,0 +1,223 @@ +// +// 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 an IDocumentSymbolProvider implementation for + /// enumerating test symbols in Pester test (tests.ps1) files. + /// + public class PesterDocumentSymbolProvider : IDocumentSymbolProvider + { + + IEnumerable IDocumentSymbolProvider.ProvideDocumentSymbols( + ScriptFile scriptFile) + { + if (!scriptFile.FilePath.EndsWith( + "tests.ps1", + StringComparison.OrdinalIgnoreCase)) + { + return Enumerable.Empty(); + } + + // Find plausible Pester commands + IEnumerable commandAsts = scriptFile.ScriptAst.FindAll(IsNamedCommandWithArguments, true); + + return commandAsts.OfType() + .Where(IsPesterCommand) + .Select(ast => ConvertPesterAstToSymbolReference(scriptFile, ast)); + } + + /// + /// Test if the given Ast is a regular CommandAst with arguments + /// + /// the PowerShell Ast to test + /// true if the Ast represents a PowerShell command with arguments, false otherwise + private static bool IsNamedCommandWithArguments(Ast ast) + { + CommandAst commandAst = ast as CommandAst; + + return commandAst != null && + commandAst.InvocationOperator != TokenKind.Dot && + PesterSymbolReference.GetCommandType(commandAst.GetCommandName()).HasValue && + commandAst.CommandElements.Count >= 2; + } + + /// + /// Test whether the given CommandAst represents a Pester command + /// + /// the CommandAst to test + /// true if the CommandAst represents a Pester command, false otherwise + private static bool IsPesterCommand(CommandAst commandAst) + { + if (commandAst == null) + { + return false; + } + + // Ensure the first word is a Pester keyword + if (!PesterSymbolReference.PesterKeywords.ContainsKey(commandAst.GetCommandName())) + { + return false; + } + + // Ensure that the last argument of the command is a scriptblock + if (!(commandAst.CommandElements[commandAst.CommandElements.Count-1] is ScriptBlockExpressionAst)) + { + return false; + } + + return true; + } + + /// + /// Convert a CommandAst known to represent a Pester command and a reference to the scriptfile + /// it is in into symbol representing a Pester call for code lens + /// + /// the scriptfile the Pester call occurs in + /// the CommandAst representing the Pester call + /// a symbol representing the Pester call containing metadata for CodeLens to use + private static PesterSymbolReference ConvertPesterAstToSymbolReference(ScriptFile scriptFile, CommandAst pesterCommandAst) + { + string testLine = scriptFile.GetLine(pesterCommandAst.Extent.StartLineNumber); + PesterCommandType? commandName = PesterSymbolReference.GetCommandType(pesterCommandAst.GetCommandName()); + if (commandName == null) + { + return null; + } + + // Search for a name for the test + // If the test has more than one argument for names, we set it to null + string testName = null; + bool alreadySawName = false; + for (int i = 1; i < pesterCommandAst.CommandElements.Count; i++) + { + CommandElementAst currentCommandElement = pesterCommandAst.CommandElements[i]; + + // Check for an explicit "-Name" parameter + if (currentCommandElement is CommandParameterAst parameterAst) + { + // Found -Name parameter, move to next element which is the argument for -TestName + i++; + + if (!alreadySawName && TryGetTestNameArgument(pesterCommandAst.CommandElements[i], out testName)) + { + alreadySawName = true; + } + + continue; + } + + // Otherwise, if an argument is given with no parameter, we assume it's the name + // If we've already seen a name, we set the name to null + if (!alreadySawName && TryGetTestNameArgument(pesterCommandAst.CommandElements[i], out testName)) + { + alreadySawName = true; + } + } + + return new PesterSymbolReference( + scriptFile, + commandName.Value, + testLine, + testName, + pesterCommandAst.Extent + ); + } + + private static bool TryGetTestNameArgument(CommandElementAst commandElementAst, out string testName) + { + testName = null; + + if (commandElementAst is StringConstantExpressionAst testNameStrAst) + { + testName = testNameStrAst.Value; + return true; + } + + return (commandElementAst is ExpandableStringExpressionAst); + } + } + + /// + /// Defines command types for Pester test blocks. + /// + public enum PesterCommandType + { + /// + /// Identifies a Describe block. + /// + Describe, + + /// + /// Identifies a Context block. + /// + Context, + + /// + /// Identifies an It block. + /// + It + } + + /// + /// Provides a specialization of SymbolReference containing + /// extra information about Pester test symbols. + /// + public class PesterSymbolReference : SymbolReference + { + /// + /// Lookup for Pester keywords we support. Ideally we could extract these from Pester itself + /// + internal static readonly IReadOnlyDictionary PesterKeywords = + Enum.GetValues(typeof(PesterCommandType)) + .Cast() + .ToDictionary(pct => pct.ToString(), pct => pct, StringComparer.OrdinalIgnoreCase); + + private static char[] DefinitionTrimChars = new char[] { ' ', '{' }; + + /// + /// Gets the name of the test + /// + public string TestName { get; private set; } + + /// + /// Gets the test's command type. + /// + public PesterCommandType Command { get; private set; } + + internal PesterSymbolReference( + ScriptFile scriptFile, + PesterCommandType commandType, + string testLine, + string testName, + IScriptExtent scriptExtent) + : base( + SymbolType.Function, + testLine.TrimEnd(DefinitionTrimChars), + scriptExtent, + scriptFile.FilePath, + testLine) + { + this.Command = commandType; + this.TestName = testName; + } + + internal static PesterCommandType? GetCommandType(string commandName) + { + PesterCommandType pesterCommandType; + if (commandName == null || !PesterKeywords.TryGetValue(commandName, out pesterCommandType)) + { + return null; + } + return pesterCommandType; + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/Symbols/PsdDocumentSymbolProvider.cs b/src/PowerShellEditorServices.Engine/Services/Symbols/PsdDocumentSymbolProvider.cs new file mode 100644 index 000000000..695ed2c02 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/Symbols/PsdDocumentSymbolProvider.cs @@ -0,0 +1,88 @@ +// +// 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 an IDocumentSymbolProvider implementation for + /// enumerating symbols in .psd1 files. + /// + public class PsdDocumentSymbolProvider : IDocumentSymbolProvider + { + IEnumerable IDocumentSymbolProvider.ProvideDocumentSymbols( + ScriptFile scriptFile) + { + if ((scriptFile.FilePath != null && + scriptFile.FilePath.EndsWith(".psd1", StringComparison.OrdinalIgnoreCase)) || + IsPowerShellDataFileAst(scriptFile.ScriptAst)) + { + var findHashtableSymbolsVisitor = new FindHashtableSymbolsVisitor(); + scriptFile.ScriptAst.Visit(findHashtableSymbolsVisitor); + return findHashtableSymbolsVisitor.SymbolReferences; + } + + return Enumerable.Empty(); + } + + /// + /// 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; + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/Symbols/ScriptDocumentSymbolProvider.cs b/src/PowerShellEditorServices.Engine/Services/Symbols/ScriptDocumentSymbolProvider.cs new file mode 100644 index 000000000..2c11747f2 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/Symbols/ScriptDocumentSymbolProvider.cs @@ -0,0 +1,76 @@ +// +// 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 an IDocumentSymbolProvider implementation for + /// enumerating symbols in script (.psd1, .psm1) files. + /// + public class ScriptDocumentSymbolProvider : IDocumentSymbolProvider + { + private Version powerShellVersion; + + /// + /// Creates an instance of the ScriptDocumentSymbolProvider to + /// target the specified PowerShell version. + /// + /// The target PowerShell version. + public ScriptDocumentSymbolProvider(Version powerShellVersion) + { + this.powerShellVersion = powerShellVersion; + } + + IEnumerable IDocumentSymbolProvider.ProvideDocumentSymbols( + ScriptFile scriptFile) + { + if (scriptFile != null && + scriptFile.FilePath != null && + (scriptFile.FilePath.EndsWith(".ps1", StringComparison.OrdinalIgnoreCase) || + scriptFile.FilePath.EndsWith(".psm1", StringComparison.OrdinalIgnoreCase))) + { + return + FindSymbolsInDocument( + scriptFile.ScriptAst, + this.powerShellVersion); + } + + return Enumerable.Empty(); + } + + /// + /// 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; + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/Symbols/ScriptExtent.cs b/src/PowerShellEditorServices.Engine/Services/Symbols/ScriptExtent.cs new file mode 100644 index 000000000..d695de649 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/Symbols/ScriptExtent.cs @@ -0,0 +1,109 @@ +// +// 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 +{ + /// + /// Provides a default IScriptExtent implementation + /// containing details about a section of script content + /// in a file. + /// + public class ScriptExtent : IScriptExtent + { + #region Properties + + /// + /// Gets the file path of the script file in which this extent is contained. + /// + public string File + { + get; + set; + } + + /// + /// Gets or sets the starting column number of the extent. + /// + public int StartColumnNumber + { + get; + set; + } + + /// + /// Gets or sets the starting line number of the extent. + /// + public int StartLineNumber + { + get; + set; + } + + /// + /// Gets or sets the starting file offset of the extent. + /// + public int StartOffset + { + get; + set; + } + + /// + /// Gets or sets the starting script position of the extent. + /// + public IScriptPosition StartScriptPosition + { + get { throw new NotImplementedException(); } + } + /// + /// Gets or sets the text that is contained within the extent. + /// + public string Text + { + get; + set; + } + + /// + /// Gets or sets the ending column number of the extent. + /// + public int EndColumnNumber + { + get; + set; + } + + /// + /// Gets or sets the ending line number of the extent. + /// + public int EndLineNumber + { + get; + set; + } + + /// + /// Gets or sets the ending file offset of the extent. + /// + public int EndOffset + { + get; + set; + } + + /// + /// Gets the ending script position of the extent. + /// + public IScriptPosition EndScriptPosition + { + get { throw new NotImplementedException(); } + } + + #endregion + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/Symbols/SymbolReference.cs b/src/PowerShellEditorServices.Engine/Services/Symbols/SymbolReference.cs new file mode 100644 index 000000000..643ab430b --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/Symbols/SymbolReference.cs @@ -0,0 +1,89 @@ +// +// 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.Diagnostics; +using System.Management.Automation.Language; + +namespace Microsoft.PowerShell.EditorServices.Symbols +{ + /// + /// A class that holds the type, name, script extent, and source line of a symbol + /// + [DebuggerDisplay("SymbolType = {SymbolType}, SymbolName = {SymbolName}")] + public class SymbolReference + { + #region Properties + + /// + /// Gets the symbol's type + /// + public SymbolType SymbolType { get; private set; } + + /// + /// Gets the name of the symbol + /// + public string SymbolName { get; private set; } + + /// + /// Gets the script extent of the symbol + /// + public ScriptRegion ScriptRegion { get; private set; } + + /// + /// Gets the contents of the line the given symbol is on + /// + public string SourceLine { get; internal set; } + + /// + /// Gets the path of the file in which the symbol was found. + /// + public string FilePath { get; internal set; } + + #endregion + + /// + /// Constructs and instance of a SymbolReference + /// + /// The higher level type of the symbol + /// The name of the symbol + /// The script extent of the symbol + /// The file path of the symbol + /// The line contents of the given symbol (defaults to empty string) + public SymbolReference( + SymbolType symbolType, + string symbolName, + IScriptExtent scriptExtent, + string filePath = "", + string sourceLine = "") + { + // TODO: Verify params + this.SymbolType = symbolType; + this.SymbolName = symbolName; + this.ScriptRegion = ScriptRegion.Create(scriptExtent); + this.FilePath = filePath; + this.SourceLine = sourceLine; + + // TODO: Make sure end column number usage is correct + + // Build the display string + //this.DisplayString = + // string.Format( + // "{0} {1}") + } + + /// + /// Constructs and instance of a SymbolReference + /// + /// The higher level type of the symbol + /// The script extent of the symbol + /// The file path of the symbol + /// The line contents of the given symbol (defaults to empty string) + public SymbolReference(SymbolType symbolType, IScriptExtent scriptExtent, string filePath = "", string sourceLine = "") + : this(symbolType, scriptExtent.Text, scriptExtent, filePath, sourceLine) + { + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/Symbols/SymbolType.cs b/src/PowerShellEditorServices.Engine/Services/Symbols/SymbolType.cs new file mode 100644 index 000000000..2dba9a0a0 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/Symbols/SymbolType.cs @@ -0,0 +1,48 @@ +// +// 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 +{ + /// + /// A way to define symbols on a higher level + /// + public enum SymbolType + { + /// + /// The symbol type is unknown + /// + Unknown = 0, + + /// + /// The symbol is a vairable + /// + Variable, + + /// + /// The symbol is a function + /// + Function, + + /// + /// The symbol is a parameter + /// + Parameter, + + /// + /// The symbol is a DSC configuration + /// + Configuration, + + /// + /// The symbol is a workflow + /// + Workflow, + + /// + /// The symbol is a hashtable key + /// + HashtableKey + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/Symbols/SymbolsService.cs b/src/PowerShellEditorServices.Engine/Services/Symbols/SymbolsService.cs new file mode 100644 index 000000000..1b4a38b19 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/Symbols/SymbolsService.cs @@ -0,0 +1,72 @@ +using System.Collections.Generic; +using Microsoft.Extensions.Logging; +using Microsoft.PowerShell.EditorServices.Symbols; + +namespace Microsoft.PowerShell.EditorServices +{ + /// + /// Provides a high-level service for performing code completion and + /// navigation operations on PowerShell scripts. + /// + public class SymbolsService + { + #region Private Fields + + const int DefaultWaitTimeoutMilliseconds = 5000; + + private readonly ILogger _logger; + + private readonly IDocumentSymbolProvider[] _documentSymbolProviders; + + #endregion + + #region Constructors + + /// + /// Constructs an instance of the SymbolsService class and uses + /// the given Runspace to execute language service operations. + /// + /// + /// The PowerShellContext in which language service operations will be executed. + /// + /// An ILogger implementation used for writing log messages. + public SymbolsService( + ILoggerFactory factory) + { + _logger = factory.CreateLogger(); + _documentSymbolProviders = new IDocumentSymbolProvider[] + { + new ScriptDocumentSymbolProvider(VersionUtils.PSVersion), + new PsdDocumentSymbolProvider(), + new PesterDocumentSymbolProvider() + }; + } + + #endregion + + + + /// + /// Finds all the symbols in a file. + /// + /// The ScriptFile in which the symbol can be located. + /// + public List FindSymbolsInFile(ScriptFile scriptFile) + { + Validate.IsNotNull(nameof(scriptFile), scriptFile); + + var foundOccurrences = new List(); + foreach (IDocumentSymbolProvider symbolProvider in _documentSymbolProviders) + { + foreach (SymbolReference reference in symbolProvider.ProvideDocumentSymbols(scriptFile)) + { + reference.SourceLine = scriptFile.GetLine(reference.ScriptRegion.StartLineNumber); + reference.FilePath = scriptFile.FilePath; + foundOccurrences.Add(reference); + } + } + + return foundOccurrences; + } + } +} diff --git a/src/PowerShellEditorServices.Engine/Services/TextDocument/ScriptFile.cs b/src/PowerShellEditorServices.Engine/Services/TextDocument/ScriptFile.cs index a916a1600..9253b81ad 100644 --- a/src/PowerShellEditorServices.Engine/Services/TextDocument/ScriptFile.cs +++ b/src/PowerShellEditorServices.Engine/Services/TextDocument/ScriptFile.cs @@ -61,7 +61,7 @@ public string DocumentUri { return this.ClientFilePath == null ? string.Empty - : Workspace.ConvertPathToDocumentUri(this.ClientFilePath); + : WorkspaceService.ConvertPathToDocumentUri(this.ClientFilePath); } } @@ -162,7 +162,7 @@ public ScriptFile( this.FilePath = filePath; this.ClientFilePath = clientFilePath; this.IsAnalysisEnabled = true; - this.IsInMemory = Workspace.IsPathInMemory(filePath); + this.IsInMemory = WorkspaceService.IsPathInMemory(filePath); this.powerShellVersion = powerShellVersion; // SetFileContents() calls ParseFileContents() which initializes the rest of the properties. diff --git a/src/PowerShellEditorServices.Engine/Services/Workspace/Handlers/WorkspaceSymbolsHandler.cs b/src/PowerShellEditorServices.Engine/Services/Workspace/Handlers/WorkspaceSymbolsHandler.cs index 460df9337..f4594dbfc 100644 --- a/src/PowerShellEditorServices.Engine/Services/Workspace/Handlers/WorkspaceSymbolsHandler.cs +++ b/src/PowerShellEditorServices.Engine/Services/Workspace/Handlers/WorkspaceSymbolsHandler.cs @@ -5,18 +5,24 @@ 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.Workspace.Handlers { public class WorkspaceSymbolsHandler : IWorkspaceSymbolsHandler { private readonly ILogger _logger; + private readonly SymbolsService _symbolsService; + private readonly WorkspaceService _workspaceService; - public WorkspaceSymbolsHandler(ILoggerFactory loggerFactory) { + public WorkspaceSymbolsHandler(ILoggerFactory loggerFactory, SymbolsService symbols, WorkspaceService workspace) { _logger = loggerFactory.CreateLogger(); + _symbolsService = symbols; + _workspaceService = workspace; } public object GetRegistrationOptions() @@ -29,40 +35,37 @@ public Task Handle(WorkspaceSymbolParams request, Ca { var symbols = new List(); - // foreach (ScriptFile scriptFile in editorSession.Workspace.GetOpenedFiles()) - // { - // FindOccurrencesResult foundSymbols = - // editorSession.LanguageService.FindSymbolsInFile( - // scriptFile); - - // // 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) - // { - // foreach (SymbolReference foundOccurrence in foundSymbols.FoundOccurrences) - // { - // if (!IsQueryMatch(request.Query, foundOccurrence.SymbolName)) - // { - // continue; - // } - - // var location = new Location - // { - // Uri = GetFileUri(foundOccurrence.FilePath), - // Range = GetRangeFromScriptRegion(foundOccurrence.ScriptRegion) - // }; - - // symbols.Add(new SymbolInformation - // { - // ContainerName = containerName, - // Kind = foundOccurrence.SymbolType == SymbolType.Variable ? SymbolKind.Variable : SymbolKind.Function, - // Location = location, - // Name = GetDecoratedSymbolName(foundOccurrence) - // }); - // } - // } - // } + foreach (ScriptFile scriptFile in _workspaceService.GetOpenedFiles()) + { + List foundSymbols = + _symbolsService.FindSymbolsInFile( + scriptFile); + + // TODO: Need to compute a relative path that is based on common path for all workspace files + string containerName = Path.GetFileNameWithoutExtension(scriptFile.FilePath); + + foreach (SymbolReference foundOccurrence in foundSymbols) + { + if (!IsQueryMatch(request.Query, foundOccurrence.SymbolName)) + { + continue; + } + + var location = new Location + { + Uri = PathUtils.ToUri(foundOccurrence.FilePath), + Range = GetRangeFromScriptRegion(foundOccurrence.ScriptRegion) + }; + + symbols.Add(new SymbolInformation + { + ContainerName = containerName, + Kind = foundOccurrence.SymbolType == SymbolType.Variable ? SymbolKind.Variable : SymbolKind.Function, + Location = location, + Name = GetDecoratedSymbolName(foundOccurrence) + }); + } + } _logger.LogWarning("Logging in a handler works now."); return Task.FromResult(new SymbolInformationContainer(symbols)); @@ -75,50 +78,50 @@ public void SetCapability(WorkspaceSymbolCapability capability) #region private Methods - // private bool IsQueryMatch(string query, string symbolName) - // { - // return symbolName.IndexOf(query, StringComparison.OrdinalIgnoreCase) >= 0; - // } - - // private static string GetFileUri(string filePath) - // { - // // If the file isn't untitled, return a URI-style path - // return - // !filePath.StartsWith("untitled") && !filePath.StartsWith("inmemory") - // ? new Uri("file://" + filePath).AbsoluteUri - // : filePath; - // } - - // 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 - // } - // }; - // } - - // 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 bool IsQueryMatch(string query, string symbolName) + { + return symbolName.IndexOf(query, StringComparison.OrdinalIgnoreCase) >= 0; + } + + private static string GetFileUri(string filePath) + { + // If the file isn't untitled, return a URI-style path + return + !filePath.StartsWith("untitled") && !filePath.StartsWith("inmemory") + ? new Uri("file://" + filePath).AbsoluteUri + : filePath; + } + + 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 + } + }; + } + + 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; + } #endregion } diff --git a/src/PowerShellEditorServices.Engine/Services/TextDocument/WorkspaceFileSystemWrapper.cs b/src/PowerShellEditorServices.Engine/Services/Workspace/WorkspaceFileSystemWrapper.cs similarity index 100% rename from src/PowerShellEditorServices.Engine/Services/TextDocument/WorkspaceFileSystemWrapper.cs rename to src/PowerShellEditorServices.Engine/Services/Workspace/WorkspaceFileSystemWrapper.cs diff --git a/src/PowerShellEditorServices.Engine/Services/TextDocument/Workspace.cs b/src/PowerShellEditorServices.Engine/Services/Workspace/WorkspaceService.cs similarity index 94% rename from src/PowerShellEditorServices.Engine/Services/TextDocument/Workspace.cs rename to src/PowerShellEditorServices.Engine/Services/Workspace/WorkspaceService.cs index 1ed26a7b2..32cde6f7b 100644 --- a/src/PowerShellEditorServices.Engine/Services/TextDocument/Workspace.cs +++ b/src/PowerShellEditorServices.Engine/Services/Workspace/WorkspaceService.cs @@ -11,7 +11,6 @@ using System.Text; using System.Runtime.InteropServices; using Microsoft.Extensions.FileSystemGlobbing; -using Microsoft.Extensions.FileSystemGlobbing.Abstractions; using Microsoft.Extensions.Logging; namespace Microsoft.PowerShell.EditorServices @@ -20,7 +19,7 @@ namespace Microsoft.PowerShell.EditorServices /// Manages a "workspace" of script files that are open for a particular /// editing session. Also helps to navigate references between ScriptFiles. /// - public class Workspace + public class WorkspaceService { #region Private Fields @@ -49,9 +48,9 @@ public class Workspace "**/*" }; - private ILogger logger; - private Version powerShellVersion; - private Dictionary workspaceFiles = new Dictionary(); + private readonly ILogger logger; + private readonly Version powerShellVersion; + private readonly Dictionary workspaceFiles = new Dictionary(); #endregion @@ -81,10 +80,10 @@ public class Workspace /// /// The version of PowerShell for which scripts will be parsed. /// An ILogger implementation used for writing log messages. - public Workspace(Version powerShellVersion, ILogger logger) + public WorkspaceService(ILoggerFactory factory) { - this.powerShellVersion = powerShellVersion; - this.logger = logger; + this.powerShellVersion = VersionUtils.PSVersion; + this.logger = factory.CreateLogger(); this.ExcludeFilesGlob = new List(); this.FollowSymlinks = true; } @@ -113,11 +112,11 @@ public ScriptFile CreateScriptFileFromFileBuffer(string filePath, string initial resolvedFilePath, filePath, initialBuffer, - this.powerShellVersion); + powerShellVersion); - this.workspaceFiles[keyName] = scriptFile; + workspaceFiles[keyName] = scriptFile; - this.logger.LogDebug("Opened file as in-memory buffer: " + resolvedFilePath); + logger.LogDebug("Opened file as in-memory buffer: " + resolvedFilePath); return scriptFile; } @@ -125,7 +124,7 @@ public ScriptFile CreateScriptFileFromFileBuffer(string filePath, string initial /// /// Gets an open file in the workspace. If the file isn't open but exists on the filesystem, load and return it. /// IMPORTANT: Not all documents have a backing file e.g. untitled: scheme documents. Consider using - /// instead. + /// instead. /// /// The file path at which the script resides. /// @@ -143,8 +142,7 @@ public ScriptFile GetFile(string filePath) string keyName = resolvedFilePath.ToLower(); // Make sure the file isn't already loaded into the workspace - ScriptFile scriptFile = null; - if (!this.workspaceFiles.TryGetValue(keyName, out scriptFile)) + if (!this.workspaceFiles.TryGetValue(keyName, out ScriptFile scriptFile)) { // This method allows FileNotFoundException to bubble up // if the file isn't found. @@ -234,8 +232,7 @@ public ScriptFile GetFileBuffer(string filePath, string initialBuffer) string keyName = resolvedFilePath.ToLower(); // Make sure the file isn't already loaded into the workspace - ScriptFile scriptFile = null; - if (!this.workspaceFiles.TryGetValue(keyName, out scriptFile) && initialBuffer != null) + if (!this.workspaceFiles.TryGetValue(keyName, out ScriptFile scriptFile) && initialBuffer != null) { scriptFile = new ScriptFile( @@ -357,14 +354,14 @@ bool ignoreReparsePoints yield break; } - var matcher = new Microsoft.Extensions.FileSystemGlobbing.Matcher(); + var matcher = new Matcher(); foreach (string pattern in includeGlobs) { matcher.AddInclude(pattern); } foreach (string pattern in excludeGlobs) { matcher.AddExclude(pattern); } var fsFactory = new WorkspaceFileSystemWrapperFactory( WorkspacePath, maxDepth, - DotNetFacade.IsNetCore ? s_psFileExtensionsCoreFramework : s_psFileExtensionsFullFramework, + VersionUtils.IsNetCore ? s_psFileExtensionsCoreFramework : s_psFileExtensionsFullFramework, ignoreReparsePoints, logger ); @@ -379,8 +376,8 @@ bool ignoreReparsePoints #region Private Methods /// - /// Recusrively searches through referencedFiles in scriptFiles - /// and builds a Dictonary of the file references + /// Recursively searches through referencedFiles in scriptFiles + /// and builds a Dictionary of the file references /// /// Details an contents of "root" script file /// A Dictionary of referenced script files @@ -413,7 +410,7 @@ private void RecursivelyFindReferences( resolvedScriptPath)); // Get the referenced file if it's not already in referencedScriptFiles - if (this.TryGetFile(resolvedScriptPath, out ScriptFile referencedFile)) + if (TryGetFile(resolvedScriptPath, out ScriptFile referencedFile)) { // Normalize the resolved script path and add it to the // referenced files list if it isn't there already @@ -433,7 +430,7 @@ internal string ResolveFilePath(string filePath) { if (filePath.StartsWith(@"file://")) { - filePath = Workspace.UnescapeDriveColon(filePath); + filePath = WorkspaceService.UnescapeDriveColon(filePath); // Client sent the path in URI format, extract the local path filePath = new Uri(filePath).LocalPath; } @@ -462,7 +459,7 @@ internal static bool IsPathInMemory(string filePath) // view of the current file or an untitled file. try { - // File system absoulute paths will have a URI scheme of file:. + // File system absolute paths will have a URI scheme of file:. // Other schemes like "untitled:" and "gitlens-git:" will return false for IsFile. var uri = new Uri(filePath); isInMemory = !uri.IsFile; @@ -679,7 +676,7 @@ public static string ConvertPathToDocumentUri(string path) docUriStrBld.Append(escapedPath).Replace("%2F", "/"); } - if (!DotNetFacade.IsNetCore) + if (!VersionUtils.IsNetCore) { // ' is not encoded by Uri.EscapeDataString in Windows PowerShell 5.x. // This is apparently a difference between .NET Framework and .NET Core. diff --git a/src/PowerShellEditorServices.Engine/Utility/DotNetFacade.cs b/src/PowerShellEditorServices.Engine/Utility/DotNetFacade.cs deleted file mode 100644 index 78bc22a08..000000000 --- a/src/PowerShellEditorServices.Engine/Utility/DotNetFacade.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; -using System.Runtime.InteropServices; - -namespace Microsoft.PowerShell.EditorServices -{ - /// - /// General purpose common utilities to prevent reimplementation. - /// - internal static class DotNetFacade - { - /// - /// True if we are running on .NET Core, false otherwise. - /// - public static bool IsNetCore { get; } = RuntimeInformation.FrameworkDescription.StartsWith(".NET Core", StringComparison.Ordinal); -} -} diff --git a/src/PowerShellEditorServices.Engine/Utility/PathUtils.cs b/src/PowerShellEditorServices.Engine/Utility/PathUtils.cs index 768b26f07..da9dfa3ad 100644 --- a/src/PowerShellEditorServices.Engine/Utility/PathUtils.cs +++ b/src/PowerShellEditorServices.Engine/Utility/PathUtils.cs @@ -8,5 +8,26 @@ public string WildcardUnescapePath(string path) { throw new NotImplementedException(); } + + public static Uri ToUri(string fileName) + { + fileName = fileName.Replace(":", "%3A").Replace("\\", "/"); + if (!fileName.StartsWith("/")) return new Uri($"file:///{fileName}"); + return new Uri($"file://{fileName}"); + } + + public static string FromUri(Uri uri) + { + if (uri.Segments.Length > 1) + { + // On windows of the Uri contains %3a local path + // doesn't come out as a proper windows path + if (uri.Segments[1].IndexOf("%3a", StringComparison.OrdinalIgnoreCase) > -1) + { + return FromUri(new Uri(uri.AbsoluteUri.Replace("%3a", ":").Replace("%3A", ":"))); + } + } + return uri.LocalPath; + } } } diff --git a/src/PowerShellEditorServices.Engine/Utility/VersionUtils.cs b/src/PowerShellEditorServices.Engine/Utility/VersionUtils.cs new file mode 100644 index 000000000..6a487e377 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Utility/VersionUtils.cs @@ -0,0 +1,50 @@ +using System; +using System.Reflection; +using System.Runtime.InteropServices; + +namespace Microsoft.PowerShell.EditorServices +{ + /// + /// General purpose common utilities to prevent reimplementation. + /// + internal static class VersionUtils + { + /// + /// True if we are running on .NET Core, false otherwise. + /// + public static bool IsNetCore { get; } = RuntimeInformation.FrameworkDescription.StartsWith(".NET Core", StringComparison.Ordinal); + + /// + /// Get's the Version of PowerShell being used. + /// + public static Version PSVersion { get; } = PowerShellReflectionUtils.PSVersion; + + /// + /// True if we are running in Windows PowerShell, false otherwise. + /// + public static bool IsPS5 { get; } = PSVersion.Major == 5; + + /// + /// True if we are running in PowerShell Core 6, false otherwise. + /// + public static bool IsPS6 { get; } = PSVersion.Major == 6; + + /// + /// True if we are running in PowerShell 7, false otherwise. + /// + public static bool IsPS7 { get; } = PSVersion.Major == 7; + } + + internal static class PowerShellReflectionUtils + { + + private static readonly Assembly _psRuntimeAssembly = typeof(System.Management.Automation.Runspaces.Runspace).Assembly; + private static readonly PropertyInfo _psVersionProperty = _psRuntimeAssembly.GetType("System.Management.Automation.PSVersionInfo") + .GetProperty("PSVersion", BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static); + + /// + /// Get's the Version of PowerShell being used. + /// + public static Version PSVersion { get; } = _psVersionProperty.GetValue(null) as Version; + } +}