diff --git a/src/PowerShellEditorServices.Engine/Hosting/EditorServicesHost.cs b/src/PowerShellEditorServices.Engine/Hosting/EditorServicesHost.cs index 55bf2ae2a..33c2d9524 100644 --- a/src/PowerShellEditorServices.Engine/Hosting/EditorServicesHost.cs +++ b/src/PowerShellEditorServices.Engine/Hosting/EditorServicesHost.cs @@ -310,6 +310,7 @@ private PowerShellContextService GetFullyInitializedPowerShellContext( // issues arise when redirecting stdio. var powerShellContext = new PowerShellContextService( logger, + languageServer, _featureFlags.Contains("PSReadLine") && _enableConsoleRepl); EditorServicesPSHostUserInterface hostUserInterface = diff --git a/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs b/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs index 709f0d1df..e0e677eec 100644 --- a/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs +++ b/src/PowerShellEditorServices.Engine/LanguageServer/OmnisharpLanguageServer.cs @@ -113,6 +113,7 @@ public async Task StartAsync() .WithHandler() .WithHandler() .WithHandler() + .WithHandler() .OnInitialize( async (languageServer, request) => { diff --git a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/PowerShellContextService.cs b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/PowerShellContextService.cs index cfe5f93dd..6d7dc6462 100644 --- a/src/PowerShellEditorServices.Engine/Services/PowerShellContext/PowerShellContextService.cs +++ b/src/PowerShellEditorServices.Engine/Services/PowerShellContext/PowerShellContextService.cs @@ -17,13 +17,13 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; +using Microsoft.PowerShell.EditorServices.Engine; using Microsoft.PowerShell.EditorServices.Session; using Microsoft.PowerShell.EditorServices.Utility; namespace Microsoft.PowerShell.EditorServices { using System.Management.Automation; - using Microsoft.PowerShell.EditorServices.Engine; /// /// Manages the lifetime and usage of a PowerShell session. @@ -49,6 +49,7 @@ static PowerShellContextService() private readonly SemaphoreSlim resumeRequestHandle = AsyncUtils.CreateSimpleLockingSemaphore(); + private readonly OmniSharp.Extensions.LanguageServer.Protocol.Server.ILanguageServer _languageServer; private bool isPSReadLineEnabled; private ILogger logger; private PowerShell powerShell; @@ -145,11 +146,16 @@ public RunspaceDetails CurrentRunspace /// /// Indicates whether PSReadLine should be used if possible /// - public PowerShellContextService(ILogger logger, bool isPSReadLineEnabled) + public PowerShellContextService( + ILogger logger, + OmniSharp.Extensions.LanguageServer.Protocol.Server.ILanguageServer languageServer, + bool isPSReadLineEnabled) { - + _languageServer = languageServer; this.logger = logger; this.isPSReadLineEnabled = isPSReadLineEnabled; + + ExecutionStatusChanged += PowerShellContext_ExecutionStatusChangedAsync; } /// @@ -1720,6 +1726,18 @@ private void OnExecutionStatusChanged( hadErrors)); } + /// + /// Event hook on the PowerShell context to listen for changes in script execution status + /// + /// the PowerShell context sending the execution event + /// details of the execution status change + private void PowerShellContext_ExecutionStatusChangedAsync(object sender, ExecutionStatusChangedEventArgs e) + { + _languageServer.SendNotification( + "powerShell/executionStatusChanged", + e); + } + #endregion #region Private Methods diff --git a/src/PowerShellEditorServices.Engine/Services/Symbols/SymbolsService.cs b/src/PowerShellEditorServices.Engine/Services/Symbols/SymbolsService.cs index 326bcc6d5..3a5ef3b07 100644 --- a/src/PowerShellEditorServices.Engine/Services/Symbols/SymbolsService.cs +++ b/src/PowerShellEditorServices.Engine/Services/Symbols/SymbolsService.cs @@ -6,12 +6,16 @@ using System; using System.Collections.Generic; using System.Collections.Specialized; +using System.IO; using System.Linq; using System.Management.Automation; +using System.Management.Automation.Language; using System.Runtime.InteropServices; +using System.Text.RegularExpressions; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.PowerShell.EditorServices.Symbols; +using PowerShellEditorServices.Engine.Utility; namespace Microsoft.PowerShell.EditorServices { @@ -25,6 +29,7 @@ public class SymbolsService private readonly ILogger _logger; private readonly PowerShellContextService _powerShellContextService; + private readonly WorkspaceService _workspaceService; private readonly IDocumentSymbolProvider[] _documentSymbolProviders; #endregion @@ -38,10 +43,12 @@ public class SymbolsService /// An ILoggerFactory implementation used for writing log messages. public SymbolsService( ILoggerFactory factory, - PowerShellContextService powerShellContextService) + PowerShellContextService powerShellContextService, + WorkspaceService workspaceService) { _logger = factory.CreateLogger(); _powerShellContextService = powerShellContextService; + _workspaceService = workspaceService; _documentSymbolProviders = new IDocumentSymbolProvider[] { new ScriptDocumentSymbolProvider(VersionUtils.PSVersion), @@ -320,5 +327,183 @@ await CommandHelpers.GetCommandInfoAsync( return null; } } + + /// + /// Finds the definition of a symbol in the script file or any of the + /// files that it references. + /// + /// The initial script file to be searched for the symbol's definition. + /// The symbol for which a definition will be found. + /// The resulting GetDefinitionResult for the symbol's definition. + public async Task GetDefinitionOfSymbolAsync( + ScriptFile sourceFile, + SymbolReference foundSymbol) + { + Validate.IsNotNull(nameof(sourceFile), sourceFile); + Validate.IsNotNull(nameof(foundSymbol), foundSymbol); + + ScriptFile[] referencedFiles = + _workspaceService.ExpandScriptReferences( + sourceFile); + + var filesSearched = new HashSet(StringComparer.OrdinalIgnoreCase); + + // look through the referenced files until definition is found + // or there are no more file to look through + SymbolReference foundDefinition = null; + foreach (ScriptFile scriptFile in referencedFiles) + { + foundDefinition = + AstOperations.FindDefinitionOfSymbol( + scriptFile.ScriptAst, + foundSymbol); + + filesSearched.Add(scriptFile.FilePath); + if (foundDefinition != null) + { + foundDefinition.FilePath = scriptFile.FilePath; + break; + } + + if (foundSymbol.SymbolType == SymbolType.Function) + { + // Dot-sourcing is parsed as a "Function" Symbol. + string dotSourcedPath = GetDotSourcedPath(foundSymbol, scriptFile); + if (scriptFile.FilePath == dotSourcedPath) + { + foundDefinition = new SymbolReference(SymbolType.Function, foundSymbol.SymbolName, scriptFile.ScriptAst.Extent, scriptFile.FilePath); + break; + } + } + } + + // if the definition the not found in referenced files + // look for it in all the files in the workspace + if (foundDefinition == null) + { + // Get a list of all powershell files in the workspace path + IEnumerable allFiles = _workspaceService.EnumeratePSFiles(); + foreach (string file in allFiles) + { + if (filesSearched.Contains(file)) + { + continue; + } + + foundDefinition = + AstOperations.FindDefinitionOfSymbol( + Parser.ParseFile(file, out Token[] tokens, out ParseError[] parseErrors), + foundSymbol); + + filesSearched.Add(file); + if (foundDefinition != null) + { + foundDefinition.FilePath = file; + break; + } + } + } + + // if definition is not found in file in the workspace + // look for it in the builtin commands + if (foundDefinition == null) + { + CommandInfo cmdInfo = + await CommandHelpers.GetCommandInfoAsync( + foundSymbol.SymbolName, + _powerShellContextService); + + foundDefinition = + FindDeclarationForBuiltinCommand( + cmdInfo, + foundSymbol); + } + + return foundDefinition; + } + + /// + /// Gets a path from a dot-source symbol. + /// + /// The symbol representing the dot-source expression. + /// The script file containing the symbol + /// + private string GetDotSourcedPath(SymbolReference symbol, ScriptFile scriptFile) + { + string cleanedUpSymbol = PathUtils.NormalizePathSeparators(symbol.SymbolName.Trim('\'', '"')); + string psScriptRoot = Path.GetDirectoryName(scriptFile.FilePath); + return _workspaceService.ResolveRelativeScriptPath(psScriptRoot, + Regex.Replace(cleanedUpSymbol, @"\$PSScriptRoot|\${PSScriptRoot}", psScriptRoot, RegexOptions.IgnoreCase)); + } + + private SymbolReference FindDeclarationForBuiltinCommand( + CommandInfo commandInfo, + SymbolReference foundSymbol) + { + if (commandInfo == null) + { + return null; + } + + ScriptFile[] nestedModuleFiles = + GetBuiltinCommandScriptFiles( + commandInfo.Module); + + SymbolReference foundDefinition = null; + foreach (ScriptFile nestedModuleFile in nestedModuleFiles) + { + foundDefinition = AstOperations.FindDefinitionOfSymbol( + nestedModuleFile.ScriptAst, + foundSymbol); + + if (foundDefinition != null) + { + foundDefinition.FilePath = nestedModuleFile.FilePath; + break; + } + } + + return foundDefinition; + } + + private ScriptFile[] GetBuiltinCommandScriptFiles( + PSModuleInfo moduleInfo) + { + if (moduleInfo == null) + { + return new ScriptFile[0]; + } + + string modPath = moduleInfo.Path; + List scriptFiles = new List(); + ScriptFile newFile; + + // find any files where the moduleInfo's path ends with ps1 or psm1 + // and add it to allowed script files + if (modPath.EndsWith(@".ps1", StringComparison.OrdinalIgnoreCase) || + modPath.EndsWith(@".psm1", StringComparison.OrdinalIgnoreCase)) + { + newFile = _workspaceService.GetFile(modPath); + newFile.IsAnalysisEnabled = false; + scriptFiles.Add(newFile); + } + + if (moduleInfo.NestedModules.Count > 0) + { + foreach (PSModuleInfo nestedInfo in moduleInfo.NestedModules) + { + string nestedModPath = nestedInfo.Path; + if (nestedModPath.EndsWith(@".ps1", StringComparison.OrdinalIgnoreCase) || + nestedModPath.EndsWith(@".psm1", StringComparison.OrdinalIgnoreCase)) + { + newFile = _workspaceService.GetFile(nestedModPath); + newFile.IsAnalysisEnabled = false; + scriptFiles.Add(newFile); + } + } + } + + return scriptFiles.ToArray(); + } } } diff --git a/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/DefinitionHandler.cs b/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/DefinitionHandler.cs new file mode 100644 index 000000000..407408cf9 --- /dev/null +++ b/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/DefinitionHandler.cs @@ -0,0 +1,108 @@ +// +// 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.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 DefinitionHandler : IDefinitionHandler + { + private readonly DocumentSelector _documentSelector = new DocumentSelector( + new DocumentFilter + { + Language = "powershell" + } + ); + + private readonly ILogger _logger; + private readonly SymbolsService _symbolsService; + private readonly WorkspaceService _workspaceService; + + private DefinitionCapability _capability; + + public DefinitionHandler( + 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(DefinitionParams request, CancellationToken cancellationToken) + { + ScriptFile scriptFile = + _workspaceService.GetFile( + request.TextDocument.Uri.ToString()); + + SymbolReference foundSymbol = + _symbolsService.FindSymbolAtLocation( + scriptFile, + (int) request.Position.Line + 1, + (int) request.Position.Character + 1); + + List definitionLocations = new List(); + if (foundSymbol != null) + { + SymbolReference foundDefinition = await _symbolsService.GetDefinitionOfSymbolAsync( + scriptFile, + foundSymbol); + + if (foundDefinition != null) + { + definitionLocations.Add( + new LocationOrLocationLink( + new Location + { + Uri = PathUtils.ToUri(foundDefinition.FilePath), + Range = GetRangeFromScriptRegion(foundDefinition.ScriptRegion) + })); + } + } + + return new LocationOrLocationLinks(definitionLocations); + } + + public void SetCapability(DefinitionCapability 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/Handlers/SignatureHelpHandler.cs b/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/SignatureHelpHandler.cs index a28828464..6636da589 100644 --- a/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/SignatureHelpHandler.cs +++ b/src/PowerShellEditorServices.Engine/Services/TextDocument/Handlers/SignatureHelpHandler.cs @@ -21,7 +21,7 @@ public class SignatureHelpHandler : ISignatureHelpHandler private readonly DocumentSelector _documentSelector = new DocumentSelector( new DocumentFilter() { - Pattern = "**/*.ps*1" + Language = "powershell" } ); diff --git a/src/PowerShellEditorServices.Engine/Utility/PathUtils.cs b/src/PowerShellEditorServices.Engine/Utility/PathUtils.cs index 17d43f77b..108c7d05f 100644 --- a/src/PowerShellEditorServices.Engine/Utility/PathUtils.cs +++ b/src/PowerShellEditorServices.Engine/Utility/PathUtils.cs @@ -31,11 +31,21 @@ public string WildcardUnescapePath(string path) throw new NotImplementedException(); } - public static Uri ToUri(string fileName) + public static Uri ToUri(string filePath) { - fileName = fileName.Replace(":", "%3A").Replace("\\", "/"); - if (!fileName.StartsWith("/")) return new Uri($"file:///{fileName}"); - return new Uri($"file://{fileName}"); + if (filePath.StartsWith("untitled", StringComparison.OrdinalIgnoreCase) || + filePath.StartsWith("inmemory", StringComparison.OrdinalIgnoreCase)) + { + return new Uri(filePath); + } + + filePath = filePath.Replace(":", "%3A").Replace("\\", "/"); + if (!filePath.StartsWith("/", StringComparison.OrdinalIgnoreCase)) + { + return new Uri($"file:///{filePath}"); + } + + return new Uri($"file://{filePath}"); } public static string FromUri(Uri uri) diff --git a/test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs b/test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs index 2f41e9cd7..d66b35f38 100644 --- a/test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs +++ b/test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs @@ -692,5 +692,41 @@ public async Task CanSendSignatureHelpRequest() Assert.Contains("Get-Date", signatureHelp.Signatures.First().Label); } + + [Fact] + public async Task CanSendDefinitionRequest() + { + string scriptPath = NewTestFile(@" +function CanSendDefinitionRequest { + +} + +CanSendDefinitionRequest +"); + + LocationOrLocationLinks locationOrLocationLinks = + await LanguageClient.SendRequest( + "textDocument/definition", + new DefinitionParams + { + TextDocument = new TextDocumentIdentifier + { + Uri = new Uri(scriptPath) + }, + Position = new Position + { + Line = 5, + Character = 2 + } + }); + + LocationOrLocationLink locationOrLocationLink = + Assert.Single(locationOrLocationLinks); + + Assert.Equal(1, locationOrLocationLink.Location.Range.Start.Line); + Assert.Equal(9, locationOrLocationLink.Location.Range.Start.Character); + Assert.Equal(1, locationOrLocationLink.Location.Range.End.Line); + Assert.Equal(33, locationOrLocationLink.Location.Range.End.Character); + } } }