// // Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. // using Microsoft.PowerShell.EditorServices.Debugging; using Microsoft.PowerShell.EditorServices.Extensions; using Microsoft.PowerShell.EditorServices.Protocol.LanguageServer; using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Channel; using Microsoft.PowerShell.EditorServices.Templates; using Microsoft.PowerShell.EditorServices.Utility; using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Management.Automation; using System.Management.Automation.Language; using System.Text; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using DebugAdapterMessages = Microsoft.PowerShell.EditorServices.Protocol.DebugAdapter; namespace Microsoft.PowerShell.EditorServices.Protocol.Server { public class LanguageServer { private static CancellationTokenSource s_existingRequestCancellation; private static readonly Location[] s_emptyLocationResult = new Location[0]; private static readonly CompletionItem[] s_emptyCompletionResult = new CompletionItem[0]; private static readonly SignatureInformation[] s_emptySignatureResult = new SignatureInformation[0]; private static readonly DocumentHighlight[] s_emptyHighlightResult = new DocumentHighlight[0]; private static readonly SymbolInformation[] s_emptySymbolResult = new SymbolInformation[0]; private ILogger Logger; private bool profilesLoaded; private bool consoleReplStarted; private EditorSession editorSession; private IMessageSender messageSender; private IMessageHandlers messageHandlers; private LanguageServerEditorOperations editorOperations; private LanguageServerSettings currentSettings = new LanguageServerSettings(); // The outer key is the file's uri, the inner key is a unique id for the diagnostic private Dictionary<string, Dictionary<string, MarkerCorrection>> codeActionsPerFile = new Dictionary<string, Dictionary<string, MarkerCorrection>>(); private TaskCompletionSource<bool> serverCompletedTask; public IEditorOperations EditorOperations { get { return this.editorOperations; } } /// <summary> /// Initializes a new language server that is used for handing language server protocol messages /// </summary> /// <param name="editorSession">The editor session that handles the PowerShell runspace</param> /// <param name="messageHandlers">An object that manages all of the message handlers</param> /// <param name="messageSender">The message sender</param> /// <param name="serverCompletedTask">A TaskCompletionSource<bool> that will be completed to stop the running process</param> /// <param name="logger">The logger.</param> public LanguageServer( EditorSession editorSession, IMessageHandlers messageHandlers, IMessageSender messageSender, TaskCompletionSource<bool> serverCompletedTask, ILogger logger) { this.Logger = logger; this.editorSession = editorSession; this.serverCompletedTask = serverCompletedTask; // Attach to the underlying PowerShell context to listen for changes in the runspace or execution status this.editorSession.PowerShellContext.RunspaceChanged += PowerShellContext_RunspaceChanged; this.editorSession.PowerShellContext.ExecutionStatusChanged += PowerShellContext_ExecutionStatusChanged; // Attach to ExtensionService events this.editorSession.ExtensionService.CommandAdded += ExtensionService_ExtensionAdded; this.editorSession.ExtensionService.CommandUpdated += ExtensionService_ExtensionUpdated; this.editorSession.ExtensionService.CommandRemoved += ExtensionService_ExtensionRemoved; this.messageSender = messageSender; this.messageHandlers = messageHandlers; // Create the IEditorOperations implementation this.editorOperations = new LanguageServerEditorOperations( this.editorSession, this.messageSender); this.editorSession.StartDebugService(this.editorOperations); this.editorSession.DebugService.DebuggerStopped += DebugService_DebuggerStopped; } /// <summary> /// Starts the language server client and sends the Initialize method. /// </summary> /// <returns>A Task that can be awaited for initialization to complete.</returns> public void Start() { // Register all supported message types this.messageHandlers.SetRequestHandler(ShutdownRequest.Type, this.HandleShutdownRequest); this.messageHandlers.SetEventHandler(ExitNotification.Type, this.HandleExitNotification); this.messageHandlers.SetRequestHandler(InitializeRequest.Type, this.HandleInitializeRequest); this.messageHandlers.SetEventHandler(InitializedNotification.Type, this.HandleInitializedNotification); this.messageHandlers.SetEventHandler(DidOpenTextDocumentNotification.Type, this.HandleDidOpenTextDocumentNotification); this.messageHandlers.SetEventHandler(DidCloseTextDocumentNotification.Type, this.HandleDidCloseTextDocumentNotification); this.messageHandlers.SetEventHandler(DidSaveTextDocumentNotification.Type, this.HandleDidSaveTextDocumentNotification); this.messageHandlers.SetEventHandler(DidChangeTextDocumentNotification.Type, this.HandleDidChangeTextDocumentNotification); this.messageHandlers.SetEventHandler(DidChangeConfigurationNotification<LanguageServerSettingsWrapper>.Type, this.HandleDidChangeConfigurationNotification); this.messageHandlers.SetRequestHandler(DefinitionRequest.Type, this.HandleDefinitionRequest); this.messageHandlers.SetRequestHandler(ReferencesRequest.Type, this.HandleReferencesRequest); this.messageHandlers.SetRequestHandler(CompletionRequest.Type, this.HandleCompletionRequest); this.messageHandlers.SetRequestHandler(CompletionResolveRequest.Type, this.HandleCompletionResolveRequest); this.messageHandlers.SetRequestHandler(SignatureHelpRequest.Type, this.HandleSignatureHelpRequest); this.messageHandlers.SetRequestHandler(DocumentHighlightRequest.Type, this.HandleDocumentHighlightRequest); this.messageHandlers.SetRequestHandler(HoverRequest.Type, this.HandleHoverRequest); this.messageHandlers.SetRequestHandler(WorkspaceSymbolRequest.Type, this.HandleWorkspaceSymbolRequest); this.messageHandlers.SetRequestHandler(CodeActionRequest.Type, this.HandleCodeActionRequest); this.messageHandlers.SetRequestHandler(DocumentFormattingRequest.Type, this.HandleDocumentFormattingRequest); this.messageHandlers.SetRequestHandler( DocumentRangeFormattingRequest.Type, this.HandleDocumentRangeFormattingRequest); this.messageHandlers.SetRequestHandler(ShowOnlineHelpRequest.Type, this.HandleShowOnlineHelpRequest); this.messageHandlers.SetRequestHandler(ShowHelpRequest.Type, this.HandleShowHelpRequest); this.messageHandlers.SetRequestHandler(ExpandAliasRequest.Type, this.HandleExpandAliasRequest); this.messageHandlers.SetRequestHandler(FindModuleRequest.Type, this.HandleFindModuleRequest); this.messageHandlers.SetRequestHandler(InstallModuleRequest.Type, this.HandleInstallModuleRequest); this.messageHandlers.SetRequestHandler(InvokeExtensionCommandRequest.Type, this.HandleInvokeExtensionCommandRequest); this.messageHandlers.SetRequestHandler(PowerShellVersionRequest.Type, this.HandlePowerShellVersionRequest); this.messageHandlers.SetRequestHandler(NewProjectFromTemplateRequest.Type, this.HandleNewProjectFromTemplateRequest); this.messageHandlers.SetRequestHandler(GetProjectTemplatesRequest.Type, this.HandleGetProjectTemplatesRequest); this.messageHandlers.SetRequestHandler(DebugAdapterMessages.EvaluateRequest.Type, this.HandleEvaluateRequest); this.messageHandlers.SetRequestHandler(GetPSSARulesRequest.Type, this.HandleGetPSSARulesRequest); this.messageHandlers.SetRequestHandler(SetPSSARulesRequest.Type, this.HandleSetPSSARulesRequest); this.messageHandlers.SetRequestHandler(ScriptRegionRequest.Type, this.HandleGetFormatScriptRegionRequest); this.messageHandlers.SetRequestHandler(GetPSHostProcessesRequest.Type, this.HandleGetPSHostProcessesRequest); this.messageHandlers.SetRequestHandler(CommentHelpRequest.Type, this.HandleCommentHelpRequest); // Initialize the extension service // TODO: This should be made awaited once Initialize is async! this.editorSession.ExtensionService.Initialize( this.editorOperations, this.editorSession.Components).Wait(); } protected Task Stop() { Logger.Write(LogLevel.Normal, "Language service is shutting down..."); // complete the task so that the host knows to shut down this.serverCompletedTask.SetResult(true); return Task.FromResult(true); } #region Built-in Message Handlers private async Task HandleShutdownRequest( RequestContext<object> requestContext) { // Allow the implementor to shut down gracefully await requestContext.SendResult(new object()); } private async Task HandleExitNotification( object exitParams, EventContext eventContext) { // Stop the server channel await this.Stop(); } private Task HandleInitializedNotification(InitializedParams initializedParams, EventContext eventContext) { // Can do dynamic registration of capabilities in this notification handler return Task.FromResult(true); } protected async Task HandleInitializeRequest( InitializeParams initializeParams, RequestContext<InitializeResult> requestContext) { // Grab the workspace path from the parameters editorSession.Workspace.WorkspacePath = initializeParams.RootPath; // Set the working directory of the PowerShell session to the workspace path if (editorSession.Workspace.WorkspacePath != null) { await editorSession.PowerShellContext.SetWorkingDirectory( editorSession.Workspace.WorkspacePath, isPathAlreadyEscaped: false); } await requestContext.SendResult( new InitializeResult { Capabilities = new ServerCapabilities { TextDocumentSync = TextDocumentSyncKind.Incremental, DefinitionProvider = true, ReferencesProvider = true, DocumentHighlightProvider = true, DocumentSymbolProvider = true, WorkspaceSymbolProvider = true, HoverProvider = true, CodeActionProvider = true, CodeLensProvider = new CodeLensOptions { ResolveProvider = true }, CompletionProvider = new CompletionOptions { ResolveProvider = true, TriggerCharacters = new string[] { ".", "-", ":", "\\" } }, SignatureHelpProvider = new SignatureHelpOptions { TriggerCharacters = new string[] { " " } // TODO: Other characters here? }, DocumentFormattingProvider = false, DocumentRangeFormattingProvider = false, RenameProvider = false } }); } protected async Task HandleShowHelpRequest( string helpParams, RequestContext<object> requestContext) { const string CheckHelpScript = @" [CmdletBinding()] param ( [String]$CommandName ) try { $command = Microsoft.PowerShell.Core\Get-Command $CommandName -ErrorAction Stop } catch [System.Management.Automation.CommandNotFoundException] { $PSCmdlet.ThrowTerminatingError($PSItem) } try { $helpUri = [Microsoft.PowerShell.Commands.GetHelpCodeMethods]::GetHelpUri($command) $oldSslVersion = [System.Net.ServicePointManager]::SecurityProtocol [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12 # HEAD means we don't need the content itself back, just the response header $status = (Microsoft.PowerShell.Utility\Invoke-WebRequest -Method Head -Uri $helpUri -TimeoutSec 5 -ErrorAction Stop).StatusCode if ($status -lt 400) { $null = Microsoft.PowerShell.Core\Get-Help $CommandName -Online return } } catch { # Ignore - we want to drop out to Get-Help -Full } finally { [System.Net.ServicePointManager]::SecurityProtocol = $oldSslVersion } return Microsoft.PowerShell.Core\Get-Help $CommandName -Full "; if (string.IsNullOrEmpty(helpParams)) { helpParams = "Get-Help"; } PSCommand checkHelpPSCommand = new PSCommand() .AddScript(CheckHelpScript, useLocalScope: true) .AddArgument(helpParams); // TODO: Rather than print the help in the console, we should send the string back // to VSCode to display in a help pop-up (or similar) await editorSession.PowerShellContext.ExecuteCommand<PSObject>(checkHelpPSCommand, sendOutputToHost: true); await requestContext.SendResult(null); } protected async Task HandleShowOnlineHelpRequest( string helpParams, RequestContext<object> requestContext ) { PSCommand commandDeprecated = new PSCommand() .AddCommand("Microsoft.PowerShell.Utility\\Write-Verbose") .AddParameter("Message", "'powerShell/showOnlineHelp' has been deprecated. Use 'powerShell/showHelp' instead."); await editorSession.PowerShellContext.ExecuteCommand<PSObject>(commandDeprecated, sendOutputToHost: true); await this.HandleShowHelpRequest(helpParams, requestContext); } private async Task HandleSetPSSARulesRequest( object param, RequestContext<object> requestContext) { var dynParams = param as dynamic; if (editorSession.AnalysisService != null && editorSession.AnalysisService.SettingsPath == null) { var activeRules = new List<string>(); var ruleInfos = dynParams.ruleInfos; foreach (dynamic ruleInfo in ruleInfos) { if ((Boolean)ruleInfo.isEnabled) { activeRules.Add((string)ruleInfo.name); } } editorSession.AnalysisService.ActiveRules = activeRules.ToArray(); } var sendresult = requestContext.SendResult(null); var scripFile = editorSession.Workspace.GetFile((string)dynParams.filepath); await RunScriptDiagnostics( new ScriptFile[] { scripFile }, editorSession, this.messageSender.SendEvent); await sendresult; } private async Task HandleGetFormatScriptRegionRequest( ScriptRegionRequestParams requestParams, RequestContext<ScriptRegionRequestResult> requestContext) { var scriptFile = this.editorSession.Workspace.GetFile(requestParams.FileUri); var lineNumber = requestParams.Line; var columnNumber = requestParams.Column; ScriptRegion scriptRegion = null; switch (requestParams.Character) { case "\n": // find the smallest statement ast that occupies // the element before \n or \r\n and return the extent. --lineNumber; // vscode sends the next line when pressed enter var line = scriptFile.GetLine(lineNumber); if (!String.IsNullOrEmpty(line)) { scriptRegion = this.editorSession.LanguageService.FindSmallestStatementAstRegion( scriptFile, lineNumber, line.Length); } break; case "}": scriptRegion = this.editorSession.LanguageService.FindSmallestStatementAstRegion( scriptFile, lineNumber, columnNumber); break; default: break; } await requestContext.SendResult(new ScriptRegionRequestResult { scriptRegion = scriptRegion }); } private async Task HandleGetPSSARulesRequest( object param, RequestContext<object> requestContext) { List<object> rules = null; if (editorSession.AnalysisService != null && editorSession.AnalysisService.SettingsPath == null) { rules = new List<object>(); var ruleNames = editorSession.AnalysisService.GetPSScriptAnalyzerRules(); var activeRules = editorSession.AnalysisService.ActiveRules; foreach (var ruleName in ruleNames) { rules.Add(new { name = ruleName, isEnabled = activeRules.Contains(ruleName, StringComparer.OrdinalIgnoreCase) }); } } await requestContext.SendResult(rules); } private async Task HandleInstallModuleRequest( string moduleName, RequestContext<object> requestContext ) { var script = string.Format("Install-Module -Name {0} -Scope CurrentUser", moduleName); var executeTask = editorSession.PowerShellContext.ExecuteScriptString( script, true, true).ConfigureAwait(false); await requestContext.SendResult(null); } private Task HandleInvokeExtensionCommandRequest( InvokeExtensionCommandRequest commandDetails, RequestContext<string> requestContext) { // We don't await the result of the execution here because we want // to be able to receive further messages while the editor command // is executing. This important in cases where the pipeline thread // gets blocked by something in the script like a prompt to the user. EditorContext editorContext = this.editorOperations.ConvertClientEditorContext( commandDetails.Context); Task commandTask = this.editorSession.ExtensionService.InvokeCommand( commandDetails.Name, editorContext); commandTask.ContinueWith(t => { return requestContext.SendResult(null); }); return Task.FromResult(true); } private Task HandleNewProjectFromTemplateRequest( NewProjectFromTemplateRequest newProjectArgs, RequestContext<NewProjectFromTemplateResponse> requestContext) { // Don't await the Task here so that we don't block the session this.editorSession.TemplateService .CreateFromTemplate(newProjectArgs.TemplatePath, newProjectArgs.DestinationPath) .ContinueWith( async task => { await requestContext.SendResult( new NewProjectFromTemplateResponse { CreationSuccessful = task.Result }); }); return Task.FromResult(true); } private async Task HandleGetProjectTemplatesRequest( GetProjectTemplatesRequest requestArgs, RequestContext<GetProjectTemplatesResponse> requestContext) { bool plasterInstalled = await this.editorSession.TemplateService.ImportPlasterIfInstalled(); if (plasterInstalled) { var availableTemplates = await this.editorSession.TemplateService.GetAvailableTemplates( requestArgs.IncludeInstalledModules); await requestContext.SendResult( new GetProjectTemplatesResponse { Templates = availableTemplates }); } else { await requestContext.SendResult( new GetProjectTemplatesResponse { NeedsModuleInstall = true, Templates = new TemplateDetails[0] }); } } private async Task HandleExpandAliasRequest( string content, RequestContext<string> requestContext) { var script = @" function __Expand-Alias { param($targetScript) [ref]$errors=$null $tokens = [System.Management.Automation.PsParser]::Tokenize($targetScript, $errors).Where({$_.type -eq 'command'}) | Sort-Object Start -Descending foreach ($token in $tokens) { $definition=(Get-Command ('`'+$token.Content) -CommandType Alias -ErrorAction SilentlyContinue).Definition if($definition) { $lhs=$targetScript.Substring(0, $token.Start) $rhs=$targetScript.Substring($token.Start + $token.Length) $targetScript=$lhs + $definition + $rhs } } $targetScript }"; var psCommand = new PSCommand(); psCommand.AddScript(script); await this.editorSession.PowerShellContext.ExecuteCommand<PSObject>(psCommand); psCommand = new PSCommand(); psCommand.AddCommand("__Expand-Alias").AddArgument(content); var result = await this.editorSession.PowerShellContext.ExecuteCommand<string>(psCommand); await requestContext.SendResult(result.First().ToString()); } private async Task HandleFindModuleRequest( object param, RequestContext<object> requestContext) { var psCommand = new PSCommand(); psCommand.AddScript("Find-Module | Select Name, Description"); var modules = await editorSession.PowerShellContext.ExecuteCommand<PSObject>(psCommand); var moduleList = new List<PSModuleMessage>(); if (modules != null) { foreach (dynamic m in modules) { moduleList.Add(new PSModuleMessage { Name = m.Name, Description = m.Description }); } } await requestContext.SendResult(moduleList); } protected Task HandleDidOpenTextDocumentNotification( DidOpenTextDocumentParams openParams, EventContext eventContext) { ScriptFile openedFile = editorSession.Workspace.GetFileBuffer( openParams.TextDocument.Uri, openParams.TextDocument.Text); // TODO: Get all recently edited files in the workspace this.RunScriptDiagnostics( new ScriptFile[] { openedFile }, editorSession, eventContext); Logger.Write(LogLevel.Verbose, "Finished opening document."); return Task.FromResult(true); } protected async Task HandleDidCloseTextDocumentNotification( DidCloseTextDocumentParams closeParams, EventContext eventContext) { // Find and close the file in the current session var fileToClose = editorSession.Workspace.GetFile(closeParams.TextDocument.Uri); if (fileToClose != null) { editorSession.Workspace.CloseFile(fileToClose); await ClearMarkers(fileToClose, eventContext); } Logger.Write(LogLevel.Verbose, "Finished closing document."); } protected async Task HandleDidSaveTextDocumentNotification( DidSaveTextDocumentParams saveParams, EventContext eventContext) { ScriptFile savedFile = this.editorSession.Workspace.GetFile( saveParams.TextDocument.Uri); if (savedFile != null) { if (this.editorSession.RemoteFileManager.IsUnderRemoteTempPath(savedFile.FilePath)) { await this.editorSession.RemoteFileManager.SaveRemoteFile( savedFile.FilePath); } } } protected Task HandleDidChangeTextDocumentNotification( DidChangeTextDocumentParams textChangeParams, EventContext eventContext) { List<ScriptFile> changedFiles = new List<ScriptFile>(); // A text change notification can batch multiple change requests foreach (var textChange in textChangeParams.ContentChanges) { ScriptFile changedFile = editorSession.Workspace.GetFile(textChangeParams.TextDocument.Uri); changedFile.ApplyChange( GetFileChangeDetails( textChange.Range, textChange.Text)); changedFiles.Add(changedFile); } // TODO: Get all recently edited files in the workspace this.RunScriptDiagnostics( changedFiles.ToArray(), editorSession, eventContext); return Task.FromResult(true); } protected async Task HandleDidChangeConfigurationNotification( DidChangeConfigurationParams<LanguageServerSettingsWrapper> configChangeParams, EventContext eventContext) { bool oldLoadProfiles = this.currentSettings.EnableProfileLoading; bool oldScriptAnalysisEnabled = this.currentSettings.ScriptAnalysis.Enable.HasValue ? this.currentSettings.ScriptAnalysis.Enable.Value : false; string oldScriptAnalysisSettingsPath = this.currentSettings.ScriptAnalysis?.SettingsPath; this.currentSettings.Update( configChangeParams.Settings.Powershell, this.editorSession.Workspace.WorkspacePath, this.Logger); if (!this.profilesLoaded && this.currentSettings.EnableProfileLoading && oldLoadProfiles != this.currentSettings.EnableProfileLoading) { await this.editorSession.PowerShellContext.LoadHostProfiles(); this.profilesLoaded = true; } // Wait until after profiles are loaded (or not, if that's the // case) before starting the interactive console. if (!this.consoleReplStarted) { // Start the interactive terminal this.editorSession.HostInput.StartCommandLoop(); this.consoleReplStarted = true; } // If there is a new settings file path, restart the analyzer with the new settigs. bool settingsPathChanged = false; string newSettingsPath = this.currentSettings.ScriptAnalysis.SettingsPath; if (!string.Equals(oldScriptAnalysisSettingsPath, newSettingsPath, StringComparison.OrdinalIgnoreCase)) { if (this.editorSession.AnalysisService != null) { this.editorSession.AnalysisService.SettingsPath = newSettingsPath; settingsPathChanged = true; } } // If script analysis settings have changed we need to clear & possibly update the current diagnostic records. if ((oldScriptAnalysisEnabled != this.currentSettings.ScriptAnalysis?.Enable) || settingsPathChanged) { // If the user just turned off script analysis or changed the settings path, send a diagnostics // event to clear the analysis markers that they already have. if (!this.currentSettings.ScriptAnalysis.Enable.Value || settingsPathChanged) { foreach (var scriptFile in editorSession.Workspace.GetOpenedFiles()) { await ClearMarkers(scriptFile, eventContext); } } await this.RunScriptDiagnostics( this.editorSession.Workspace.GetOpenedFiles(), this.editorSession, eventContext); } } protected async Task HandleDefinitionRequest( TextDocumentPositionParams textDocumentPosition, RequestContext<Location[]> requestContext) { ScriptFile scriptFile = editorSession.Workspace.GetFile( textDocumentPosition.TextDocument.Uri); SymbolReference foundSymbol = editorSession.LanguageService.FindSymbolAtLocation( scriptFile, textDocumentPosition.Position.Line + 1, textDocumentPosition.Position.Character + 1); List<Location> definitionLocations = new List<Location>(); GetDefinitionResult definition = null; if (foundSymbol != null) { definition = await editorSession.LanguageService.GetDefinitionOfSymbol( scriptFile, foundSymbol, editorSession.Workspace); if (definition != null) { definitionLocations.Add( new Location { Uri = GetFileUri(definition.FoundDefinition.FilePath), Range = GetRangeFromScriptRegion(definition.FoundDefinition.ScriptRegion) }); } } await requestContext.SendResult(definitionLocations.ToArray()); } protected async Task HandleReferencesRequest( ReferencesParams referencesParams, RequestContext<Location[]> requestContext) { ScriptFile scriptFile = editorSession.Workspace.GetFile( referencesParams.TextDocument.Uri); SymbolReference foundSymbol = editorSession.LanguageService.FindSymbolAtLocation( scriptFile, referencesParams.Position.Line + 1, referencesParams.Position.Character + 1); FindReferencesResult referencesResult = await editorSession.LanguageService.FindReferencesOfSymbol( foundSymbol, editorSession.Workspace.ExpandScriptReferences(scriptFile), editorSession.Workspace); Location[] referenceLocations = s_emptyLocationResult; if (referencesResult != null) { var locations = new List<Location>(); foreach (SymbolReference foundReference in referencesResult.FoundReferences) { locations.Add(new Location { Uri = GetFileUri(foundReference.FilePath), Range = GetRangeFromScriptRegion(foundReference.ScriptRegion) }); } referenceLocations = locations.ToArray(); } await requestContext.SendResult(referenceLocations); } protected async Task HandleCompletionRequest( TextDocumentPositionParams textDocumentPositionParams, RequestContext<CompletionItem[]> requestContext) { int cursorLine = textDocumentPositionParams.Position.Line + 1; int cursorColumn = textDocumentPositionParams.Position.Character + 1; ScriptFile scriptFile = editorSession.Workspace.GetFile( textDocumentPositionParams.TextDocument.Uri); CompletionResults completionResults = await editorSession.LanguageService.GetCompletionsInFile( scriptFile, cursorLine, cursorColumn); CompletionItem[] completionItems = s_emptyCompletionResult; if (completionResults != null) { completionItems = new CompletionItem[completionResults.Completions.Length]; for (int i = 0; i < completionItems.Length; i++) { completionItems[i] = CreateCompletionItem(completionResults.Completions[i], completionResults.ReplacedRange, i + 1); } } await requestContext.SendResult(completionItems); } protected async Task HandleCompletionResolveRequest( CompletionItem completionItem, RequestContext<CompletionItem> requestContext) { if (completionItem.Kind == CompletionItemKind.Function) { // Get the documentation for the function CommandInfo commandInfo = await CommandHelpers.GetCommandInfo( completionItem.Label, this.editorSession.PowerShellContext); if (commandInfo != null) { completionItem.Documentation = await CommandHelpers.GetCommandSynopsis( commandInfo, this.editorSession.PowerShellContext); } } // Send back the updated CompletionItem await requestContext.SendResult(completionItem); } protected async Task HandleSignatureHelpRequest( TextDocumentPositionParams textDocumentPositionParams, RequestContext<SignatureHelp> requestContext) { ScriptFile scriptFile = editorSession.Workspace.GetFile( textDocumentPositionParams.TextDocument.Uri); ParameterSetSignatures parameterSets = await editorSession.LanguageService.FindParameterSetsInFile( scriptFile, textDocumentPositionParams.Position.Line + 1, textDocumentPositionParams.Position.Character + 1); SignatureInformation[] signatures = s_emptySignatureResult; if (parameterSets != null) { signatures = new SignatureInformation[parameterSets.Signatures.Length]; for (int i = 0; i < signatures.Length; i++) { var parameters = new ParameterInformation[parameterSets.Signatures[i].Parameters.Count()]; int j = 0; foreach (ParameterInfo param in parameterSets.Signatures[i].Parameters) { parameters[j] = CreateParameterInfo(param); j++; } signatures[i] = new SignatureInformation { Label = parameterSets.CommandName + " " + parameterSets.Signatures[i].SignatureText, Documentation = null, Parameters = parameters, }; } } await requestContext.SendResult( new SignatureHelp { Signatures = signatures, ActiveParameter = null, ActiveSignature = 0 }); } protected async Task HandleDocumentHighlightRequest( TextDocumentPositionParams textDocumentPositionParams, RequestContext<DocumentHighlight[]> requestContext) { ScriptFile scriptFile = editorSession.Workspace.GetFile( textDocumentPositionParams.TextDocument.Uri); FindOccurrencesResult occurrencesResult = editorSession.LanguageService.FindOccurrencesInFile( scriptFile, textDocumentPositionParams.Position.Line + 1, textDocumentPositionParams.Position.Character + 1); DocumentHighlight[] documentHighlights = s_emptyHighlightResult; if (occurrencesResult != null) { var highlights = new List<DocumentHighlight>(); foreach (SymbolReference foundOccurrence in occurrencesResult.FoundOccurrences) { highlights.Add(new DocumentHighlight { Kind = DocumentHighlightKind.Write, // TODO: Which symbol types are writable? Range = GetRangeFromScriptRegion(foundOccurrence.ScriptRegion) }); } documentHighlights = highlights.ToArray(); } await requestContext.SendResult(documentHighlights); } protected async Task HandleHoverRequest( TextDocumentPositionParams textDocumentPositionParams, RequestContext<Hover> requestContext) { ScriptFile scriptFile = editorSession.Workspace.GetFile( textDocumentPositionParams.TextDocument.Uri); SymbolDetails symbolDetails = await editorSession .LanguageService .FindSymbolDetailsAtLocation( scriptFile, textDocumentPositionParams.Position.Line + 1, textDocumentPositionParams.Position.Character + 1); List<MarkedString> symbolInfo = new List<MarkedString>(); Range symbolRange = null; if (symbolDetails != null) { symbolInfo.Add( new MarkedString { Language = "PowerShell", Value = symbolDetails.DisplayString }); if (!string.IsNullOrEmpty(symbolDetails.Documentation)) { symbolInfo.Add( new MarkedString { Language = "markdown", Value = symbolDetails.Documentation }); } symbolRange = GetRangeFromScriptRegion(symbolDetails.SymbolReference.ScriptRegion); } await requestContext.SendResult( new Hover { Contents = symbolInfo.ToArray(), Range = symbolRange }); } protected async Task HandleDocumentSymbolRequest( DocumentSymbolParams documentSymbolParams, RequestContext<SymbolInformation[]> requestContext) { ScriptFile scriptFile = editorSession.Workspace.GetFile( documentSymbolParams.TextDocument.Uri); FindOccurrencesResult foundSymbols = editorSession.LanguageService.FindSymbolsInFile( scriptFile); string containerName = Path.GetFileNameWithoutExtension(scriptFile.FilePath); SymbolInformation[] symbols = s_emptySymbolResult; if (foundSymbols != null) { var symbolAcc = new List<SymbolInformation>(); foreach (SymbolReference foundOccurrence in foundSymbols.FoundOccurrences) { var location = new Location { Uri = GetFileUri(foundOccurrence.FilePath), Range = GetRangeFromScriptRegion(foundOccurrence.ScriptRegion) }; symbolAcc.Add(new SymbolInformation { ContainerName = containerName, Kind = GetSymbolKind(foundOccurrence.SymbolType), Location = location, Name = GetDecoratedSymbolName(foundOccurrence) }); } symbols = symbolAcc.ToArray(); } await requestContext.SendResult(symbols); } public static SymbolKind GetSymbolKind(SymbolType symbolType) { switch (symbolType) { case SymbolType.Configuration: case SymbolType.Function: case SymbolType.Workflow: return SymbolKind.Function; default: return SymbolKind.Variable; } } public 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; } protected async Task HandleWorkspaceSymbolRequest( WorkspaceSymbolParams workspaceSymbolParams, RequestContext<SymbolInformation[]> requestContext) { var symbols = new List<SymbolInformation>(); 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(workspaceSymbolParams.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) }); } } } await requestContext.SendResult(symbols.ToArray()); } protected async Task HandlePowerShellVersionRequest( object noParams, RequestContext<PowerShellVersion> requestContext) { await requestContext.SendResult( new PowerShellVersion( this.editorSession.PowerShellContext.LocalPowerShellVersion)); } protected async Task HandleGetPSHostProcessesRequest( object noParams, RequestContext<GetPSHostProcessesResponse[]> requestContext) { var psHostProcesses = new List<GetPSHostProcessesResponse>(); if (this.editorSession.PowerShellContext.LocalPowerShellVersion.Version.Major >= 5) { int processId = System.Diagnostics.Process.GetCurrentProcess().Id; var psCommand = new PSCommand(); psCommand.AddCommand("Get-PSHostProcessInfo"); psCommand.AddCommand("Where-Object") .AddParameter("Property", "ProcessId") .AddParameter("NE") .AddParameter("Value", processId.ToString()); var processes = await editorSession.PowerShellContext.ExecuteCommand<PSObject>(psCommand); if (processes != null) { foreach (dynamic p in processes) { psHostProcesses.Add( new GetPSHostProcessesResponse { ProcessName = p.ProcessName, ProcessId = p.ProcessId, AppDomainName = p.AppDomainName, MainWindowTitle = p.MainWindowTitle }); } } } await requestContext.SendResult(psHostProcesses.ToArray()); } protected async Task HandleCommentHelpRequest( CommentHelpRequestParams requestParams, RequestContext<CommentHelpRequestResult> requestContext) { ScriptFile scriptFile = this.editorSession.Workspace.GetFile(requestParams.DocumentUri); int triggerLine = requestParams.TriggerPosition.Line + 1; string helpLocation; FunctionDefinitionAst functionDefinitionAst = editorSession.LanguageService.GetFunctionDefinitionForHelpComment( scriptFile, triggerLine, out helpLocation); var result = new CommentHelpRequestResult(); if (functionDefinitionAst == null) { await requestContext.SendResult(result); return; } IScriptExtent funcExtent = functionDefinitionAst.Extent; string funcText = funcExtent.Text; if (helpLocation.Equals("begin")) { // check if the previous character is `<` because it invalidates // the param block the follows it. IList<string> lines = ScriptFile.GetLines(funcText); int relativeTriggerLine0b = triggerLine - funcExtent.StartLineNumber; if (relativeTriggerLine0b > 0 && lines[relativeTriggerLine0b].IndexOf("<") > -1) { lines[relativeTriggerLine0b] = string.Empty; } funcText = string.Join("\n", lines); } ScriptFileMarker[] analysisResults = await this.editorSession.AnalysisService.GetSemanticMarkersAsync( funcText, AnalysisService.GetCommentHelpRuleSettings( enable: true, exportedOnly: false, blockComment: requestParams.BlockComment, vscodeSnippetCorrection: true, placement: helpLocation)); string helpText = analysisResults?.FirstOrDefault()?.Correction?.Edits[0].Text; if (helpText == null) { await requestContext.SendResult(result); return; } result.Content = ScriptFile.GetLines(helpText).ToArray(); if (helpLocation != null && !helpLocation.Equals("before", StringComparison.OrdinalIgnoreCase)) { // we need to trim the leading `{` and newline when helpLocation=="begin" // we also need to trim the leading newline when helpLocation=="end" result.Content = result.Content.Skip(1).ToArray(); } await requestContext.SendResult(result); } private bool IsQueryMatch(string query, string symbolName) { return symbolName.IndexOf(query, StringComparison.OrdinalIgnoreCase) >= 0; } // https://microsoft.github.io/language-server-protocol/specification#textDocument_codeAction protected async Task HandleCodeActionRequest( CodeActionParams codeActionParams, RequestContext<CodeActionCommand[]> requestContext) { MarkerCorrection correction = null; Dictionary<string, MarkerCorrection> markerIndex = null; List<CodeActionCommand> codeActionCommands = new List<CodeActionCommand>(); // If there are any code fixes, send these commands first so they appear at top of "Code Fix" menu in the client UI. if (this.codeActionsPerFile.TryGetValue(codeActionParams.TextDocument.Uri, out markerIndex)) { foreach (var diagnostic in codeActionParams.Context.Diagnostics) { if (string.IsNullOrEmpty(diagnostic.Code)) { this.Logger.Write( LogLevel.Warning, $"textDocument/codeAction skipping diagnostic with empty Code field: {diagnostic.Source} {diagnostic.Message}"); continue; } string diagnosticId = GetUniqueIdFromDiagnostic(diagnostic); if (markerIndex.TryGetValue(diagnosticId, out correction)) { codeActionCommands.Add( new CodeActionCommand { Title = correction.Name, Command = "PowerShell.ApplyCodeActionEdits", Arguments = JArray.FromObject(correction.Edits) }); } } } // Add "show documentation" commands last so they appear at the bottom of the client UI. // These commands do not require code fixes. Sometimes we get a batch of diagnostics // to create commands for. No need to create multiple show doc commands for the same rule. var ruleNamesProcessed = new HashSet<string>(); foreach (var diagnostic in codeActionParams.Context.Diagnostics) { if (string.IsNullOrEmpty(diagnostic.Code)) { continue; } if (string.Equals(diagnostic.Source, "PSScriptAnalyzer", StringComparison.OrdinalIgnoreCase) && !ruleNamesProcessed.Contains(diagnostic.Code)) { ruleNamesProcessed.Add(diagnostic.Code); codeActionCommands.Add( new CodeActionCommand { Title = $"Show documentation for \"{diagnostic.Code}\"", Command = "PowerShell.ShowCodeActionDocumentation", Arguments = JArray.FromObject(new[] { diagnostic.Code }) }); } } await requestContext.SendResult( codeActionCommands.ToArray()); } protected async Task HandleDocumentFormattingRequest( DocumentFormattingParams formattingParams, RequestContext<TextEdit[]> requestContext) { var result = await Format( formattingParams.TextDocument.Uri, formattingParams.options, null); await requestContext.SendResult(new TextEdit[1] { new TextEdit { NewText = result.Item1, Range = result.Item2 }, }); } protected async Task HandleDocumentRangeFormattingRequest( DocumentRangeFormattingParams formattingParams, RequestContext<TextEdit[]> requestContext) { var result = await Format( formattingParams.TextDocument.Uri, formattingParams.Options, formattingParams.Range); await requestContext.SendResult(new TextEdit[1] { new TextEdit { NewText = result.Item1, Range = result.Item2 }, }); } protected Task HandleEvaluateRequest( DebugAdapterMessages.EvaluateRequestArguments evaluateParams, RequestContext<DebugAdapterMessages.EvaluateResponseBody> requestContext) { // We don't await the result of the execution here because we want // to be able to receive further messages while the current script // is executing. This important in cases where the pipeline thread // gets blocked by something in the script like a prompt to the user. var executeTask = this.editorSession.PowerShellContext.ExecuteScriptString( evaluateParams.Expression, writeInputToHost: true, writeOutputToHost: true, addToHistory: true); // Return the execution result after the task completes so that the // caller knows when command execution completed. executeTask.ContinueWith( (task) => { // Return an empty result since the result value is irrelevant // for this request in the LanguageServer return requestContext.SendResult( new DebugAdapterMessages.EvaluateResponseBody { Result = "", VariablesReference = 0 }); }); return Task.FromResult(true); } #endregion #region Event Handlers private async Task<Tuple<string, Range>> Format( string documentUri, FormattingOptions options, Range range) { var scriptFile = editorSession.Workspace.GetFile(documentUri); var pssaSettings = currentSettings.CodeFormatting.GetPSSASettingsHashtable( options.TabSize, options.InsertSpaces); // TODO raise an error event in case format returns null; string formattedScript; Range editRange; var rangeList = range == null ? null : new int[] { range.Start.Line + 1, range.Start.Character + 1, range.End.Line + 1, range.End.Character + 1}; var extent = scriptFile.ScriptAst.Extent; // todo create an extension for converting range to script extent editRange = new Range { Start = new Position { Line = extent.StartLineNumber - 1, Character = extent.StartColumnNumber - 1 }, End = new Position { Line = extent.EndLineNumber - 1, Character = extent.EndColumnNumber - 1 } }; formattedScript = await editorSession.AnalysisService.Format( scriptFile.Contents, pssaSettings, rangeList); formattedScript = formattedScript ?? scriptFile.Contents; return Tuple.Create(formattedScript, editRange); } private async void PowerShellContext_RunspaceChanged(object sender, Session.RunspaceChangedEventArgs e) { await this.messageSender.SendEvent( RunspaceChangedEvent.Type, new Protocol.LanguageServer.RunspaceDetails(e.NewRunspace)); } /// <summary> /// Event hook on the PowerShell context to listen for changes in script execution status /// </summary> /// <param name="sender">the PowerShell context sending the execution event</param> /// <param name="e">details of the execution status change</param> private async void PowerShellContext_ExecutionStatusChanged(object sender, ExecutionStatusChangedEventArgs e) { await this.messageSender.SendEvent( ExecutionStatusChangedEvent.Type, e); } private async void ExtensionService_ExtensionAdded(object sender, EditorCommand e) { await this.messageSender.SendEvent( ExtensionCommandAddedNotification.Type, new ExtensionCommandAddedNotification { Name = e.Name, DisplayName = e.DisplayName }); } private async void ExtensionService_ExtensionUpdated(object sender, EditorCommand e) { await this.messageSender.SendEvent( ExtensionCommandUpdatedNotification.Type, new ExtensionCommandUpdatedNotification { Name = e.Name, }); } private async void ExtensionService_ExtensionRemoved(object sender, EditorCommand e) { await this.messageSender.SendEvent( ExtensionCommandRemovedNotification.Type, new ExtensionCommandRemovedNotification { Name = e.Name, }); } private async void DebugService_DebuggerStopped(object sender, DebuggerStoppedEventArgs e) { if (!this.editorSession.DebugService.IsClientAttached) { await this.messageSender.SendEvent( StartDebuggerEvent.Type, new StartDebuggerEvent()); } } #endregion #region Helper Methods public 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; } public 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 FileChange GetFileChangeDetails(Range changeRange, string insertString) { // The protocol's positions are zero-based so add 1 to all offsets if (changeRange == null) return new FileChange { InsertString = insertString, IsReload = true }; return new FileChange { InsertString = insertString, Line = changeRange.Start.Line + 1, Offset = changeRange.Start.Character + 1, EndLine = changeRange.End.Line + 1, EndOffset = changeRange.End.Character + 1, IsReload = false }; } private Task RunScriptDiagnostics( ScriptFile[] filesToAnalyze, EditorSession editorSession, EventContext eventContext) { return RunScriptDiagnostics(filesToAnalyze, editorSession, this.messageSender.SendEvent); } private Task RunScriptDiagnostics( ScriptFile[] filesToAnalyze, EditorSession editorSession, Func<NotificationType<PublishDiagnosticsNotification, object>, PublishDiagnosticsNotification, Task> eventSender) { // If there's an existing task, attempt to cancel it try { if (s_existingRequestCancellation != null) { // Try to cancel the request s_existingRequestCancellation.Cancel(); // If cancellation didn't throw an exception, // clean up the existing token s_existingRequestCancellation.Dispose(); s_existingRequestCancellation = null; } } catch (Exception e) { // TODO: Catch a more specific exception! Logger.Write( LogLevel.Error, string.Format( "Exception while canceling analysis task:\n\n{0}", e.ToString())); TaskCompletionSource<bool> cancelTask = new TaskCompletionSource<bool>(); cancelTask.SetCanceled(); return cancelTask.Task; } // If filesToAnalzye is empty, nothing to do so return early. if (filesToAnalyze.Length == 0) { return Task.FromResult(true); } // Create a fresh cancellation token and then start the task. // We create this on a different TaskScheduler so that we // don't block the main message loop thread. // TODO: Is there a better way to do this? s_existingRequestCancellation = new CancellationTokenSource(); Task.Factory.StartNew( () => DelayThenInvokeDiagnostics( 750, filesToAnalyze, this.currentSettings.ScriptAnalysis?.Enable.Value ?? false, this.codeActionsPerFile, editorSession, eventSender, this.Logger, s_existingRequestCancellation.Token), CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default); return Task.FromResult(true); } private static async Task DelayThenInvokeDiagnostics( int delayMilliseconds, ScriptFile[] filesToAnalyze, bool isScriptAnalysisEnabled, Dictionary<string, Dictionary<string, MarkerCorrection>> correctionIndex, EditorSession editorSession, Func<NotificationType<PublishDiagnosticsNotification, object>, PublishDiagnosticsNotification, Task> eventSender, ILogger Logger, CancellationToken cancellationToken) { // First of all, wait for the desired delay period before // analyzing the provided list of files try { await Task.Delay(delayMilliseconds, cancellationToken); } catch (TaskCanceledException) { // If the task is cancelled, exit directly return; } // If we've made it past the delay period then we don't care // about the cancellation token anymore. This could happen // when the user stops typing for long enough that the delay // period ends but then starts typing while analysis is going // on. It makes sense to send back the results from the first // delay period while the second one is ticking away. // Get the requested files foreach (ScriptFile scriptFile in filesToAnalyze) { ScriptFileMarker[] semanticMarkers = null; if (isScriptAnalysisEnabled && editorSession.AnalysisService != null) { using (Logger.LogExecutionTime($"Script analysis of {scriptFile.FilePath} completed.")) { semanticMarkers = await editorSession.AnalysisService.GetSemanticMarkersAsync(scriptFile); } } else { // Semantic markers aren't available if the AnalysisService // isn't available semanticMarkers = new ScriptFileMarker[0]; } await PublishScriptDiagnostics( scriptFile, // Concat script analysis errors to any existing parse errors scriptFile.SyntaxMarkers.Concat(semanticMarkers).ToArray(), correctionIndex, eventSender); } } private async Task ClearMarkers(ScriptFile scriptFile, EventContext eventContext) { // send empty diagnostic markers to clear any markers associated with the given file await PublishScriptDiagnostics( scriptFile, new ScriptFileMarker[0], this.codeActionsPerFile, eventContext); } private static async Task PublishScriptDiagnostics( ScriptFile scriptFile, ScriptFileMarker[] markers, Dictionary<string, Dictionary<string, MarkerCorrection>> correctionIndex, EventContext eventContext) { await PublishScriptDiagnostics( scriptFile, markers, correctionIndex, eventContext.SendEvent); } private static async Task PublishScriptDiagnostics( ScriptFile scriptFile, ScriptFileMarker[] markers, Dictionary<string, Dictionary<string, MarkerCorrection>> correctionIndex, Func<NotificationType<PublishDiagnosticsNotification, object>, PublishDiagnosticsNotification, Task> eventSender) { List<Diagnostic> diagnostics = new List<Diagnostic>(); // Hold on to any corrections that may need to be applied later Dictionary<string, MarkerCorrection> fileCorrections = new Dictionary<string, MarkerCorrection>(); foreach (var marker in markers) { // Does the marker contain a correction? Diagnostic markerDiagnostic = GetDiagnosticFromMarker(marker); if (marker.Correction != null) { string diagnosticId = GetUniqueIdFromDiagnostic(markerDiagnostic); fileCorrections.Add(diagnosticId, marker.Correction); } diagnostics.Add(markerDiagnostic); } correctionIndex[scriptFile.ClientFilePath] = fileCorrections; // Always send syntax and semantic errors. We want to // make sure no out-of-date markers are being displayed. await eventSender( PublishDiagnosticsNotification.Type, new PublishDiagnosticsNotification { Uri = scriptFile.ClientFilePath, Diagnostics = diagnostics.ToArray() }); } // Generate a unique id that is used as a key to look up the associated code action (code fix) when // we receive and process the textDocument/codeAction message. private static string GetUniqueIdFromDiagnostic(Diagnostic diagnostic) { Position start = diagnostic.Range.Start; Position end = diagnostic.Range.End; var sb = new StringBuilder(256); sb.Append(diagnostic.Source ?? "?"); sb.Append("_"); sb.Append(diagnostic.Code ?? "?"); sb.Append("_"); sb.Append((diagnostic.Severity != null) ? diagnostic.Severity.ToString() : "?"); sb.Append("_"); sb.Append(start.Line); sb.Append(":"); sb.Append(start.Character); sb.Append("-"); sb.Append(end.Line); sb.Append(":"); sb.Append(end.Character); return sb.ToString(); } private static Diagnostic GetDiagnosticFromMarker(ScriptFileMarker scriptFileMarker) { return new Diagnostic { Severity = MapDiagnosticSeverity(scriptFileMarker.Level), Message = scriptFileMarker.Message, Code = scriptFileMarker.RuleName, Source = scriptFileMarker.Source, Range = new Range { Start = new Position { Line = scriptFileMarker.ScriptRegion.StartLineNumber - 1, Character = scriptFileMarker.ScriptRegion.StartColumnNumber - 1 }, End = new Position { Line = scriptFileMarker.ScriptRegion.EndLineNumber - 1, Character = scriptFileMarker.ScriptRegion.EndColumnNumber - 1 } } }; } private static CompletionItemKind MapCompletionKind(CompletionType completionType) { switch (completionType) { case CompletionType.Command: return CompletionItemKind.Function; case CompletionType.Property: return CompletionItemKind.Property; case CompletionType.Method: return CompletionItemKind.Method; case CompletionType.Variable: case CompletionType.ParameterName: return CompletionItemKind.Variable; case CompletionType.File: return CompletionItemKind.File; case CompletionType.Folder: return CompletionItemKind.Folder; default: return CompletionItemKind.Text; } } private static CompletionItem CreateCompletionItem( CompletionDetails completionDetails, BufferRange completionRange, int sortIndex) { string detailString = null; string documentationString = null; if ((completionDetails.CompletionType == CompletionType.Variable) || (completionDetails.CompletionType == CompletionType.ParameterName)) { // Look for type encoded in the tooltip for parameters and variables. // Display PowerShell type names in [] to be consistent with PowerShell syntax // and now the debugger displays type names. var matches = Regex.Matches(completionDetails.ToolTipText, @"^(\[.+\])"); if ((matches.Count > 0) && (matches[0].Groups.Count > 1)) { detailString = matches[0].Groups[1].Value; } } else if ((completionDetails.CompletionType == CompletionType.Method) || (completionDetails.CompletionType == CompletionType.Property)) { // We have a raw signature for .NET members, heck let's display it. It's // better than nothing. documentationString = completionDetails.ToolTipText; } else if (completionDetails.CompletionType == CompletionType.Command) { // For Commands, let's extract the resolved command or the path for an exe // from the ToolTipText - if there is any ToolTipText. if (completionDetails.ToolTipText != null) { // Fix for #240 - notepad++.exe in tooltip text caused regex parser to throw. string escapedToolTipText = Regex.Escape(completionDetails.ToolTipText); // Don't display ToolTipText if it is the same as the ListItemText. // Reject command syntax ToolTipText - it's too much to display as a detailString. if (!completionDetails.ListItemText.Equals( completionDetails.ToolTipText, StringComparison.OrdinalIgnoreCase) && !Regex.IsMatch(completionDetails.ToolTipText, @"^\s*" + escapedToolTipText + @"\s+\[")) { detailString = completionDetails.ToolTipText; } } } // Force the client to maintain the sort order in which the // original completion results were returned. We just need to // make sure the default order also be the lexicographical order // which we do by prefixing the ListItemText with a leading 0's // four digit index. var sortText = $"{sortIndex:D4}{completionDetails.ListItemText}"; return new CompletionItem { InsertText = completionDetails.CompletionText, Label = completionDetails.ListItemText, Kind = MapCompletionKind(completionDetails.CompletionType), Detail = detailString, Documentation = documentationString, SortText = sortText, FilterText = completionDetails.CompletionText, TextEdit = new TextEdit { NewText = completionDetails.CompletionText, Range = new Range { Start = new Position { Line = completionRange.Start.Line - 1, Character = completionRange.Start.Column - 1 }, End = new Position { Line = completionRange.End.Line - 1, Character = completionRange.End.Column - 1 } } } }; } private static DiagnosticSeverity MapDiagnosticSeverity(ScriptFileMarkerLevel markerLevel) { switch (markerLevel) { case ScriptFileMarkerLevel.Error: return DiagnosticSeverity.Error; case ScriptFileMarkerLevel.Warning: return DiagnosticSeverity.Warning; case ScriptFileMarkerLevel.Information: return DiagnosticSeverity.Information; default: return DiagnosticSeverity.Error; } } private static ParameterInformation CreateParameterInfo(ParameterInfo parameterInfo) { return new ParameterInformation { Label = parameterInfo.Name, Documentation = string.Empty }; } #endregion } }