diff --git a/src/PowerShellEditorServices.Protocol/LanguageServer/CommentHelpRequest.cs b/src/PowerShellEditorServices.Protocol/LanguageServer/CommentHelpRequest.cs new file mode 100644 index 000000000..d4406519f --- /dev/null +++ b/src/PowerShellEditorServices.Protocol/LanguageServer/CommentHelpRequest.cs @@ -0,0 +1,28 @@ +// +// 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.Protocol.MessageProtocol; + +namespace Microsoft.PowerShell.EditorServices.Protocol.LanguageServer +{ + class CommentHelpRequest + { + public static readonly RequestType Type + = RequestType.Create("powerShell/getCommentHelp"); + } + + public class CommentHelpRequestResult + { + public string[] Content { get; set; } + } + + public class CommentHelpRequestParams + { + public string DocumentUri { get; set; } + public Position TriggerPosition { get; set; } + public bool BlockComment { get; set; } + } +} + diff --git a/src/PowerShellEditorServices.Protocol/Server/LanguageServer.cs b/src/PowerShellEditorServices.Protocol/Server/LanguageServer.cs index b22c11504..c2978e18e 100644 --- a/src/PowerShellEditorServices.Protocol/Server/LanguageServer.cs +++ b/src/PowerShellEditorServices.Protocol/Server/LanguageServer.cs @@ -20,6 +20,7 @@ using System.Threading; using System.Threading.Tasks; using DebugAdapterMessages = Microsoft.PowerShell.EditorServices.Protocol.DebugAdapter; +using System.Collections; namespace Microsoft.PowerShell.EditorServices.Protocol.Server { @@ -147,7 +148,7 @@ protected override void Initialize() this.SetRequestHandler(ScriptRegionRequest.Type, this.HandleGetFormatScriptRegionRequest); this.SetRequestHandler(GetPSHostProcessesRequest.Type, this.HandleGetPSHostProcessesRequest); - + this.SetRequestHandler(CommentHelpRequest.Type, this.HandleCommentHelpRequest); // Initialize the extension service // TODO: This should be made awaited once Initialize is async! this.editorSession.ExtensionService.Initialize( @@ -241,9 +242,9 @@ private async Task HandleSetPSSARulesRequest( var ruleInfos = dynParams.ruleInfos; foreach (dynamic ruleInfo in ruleInfos) { - if ((Boolean) ruleInfo.isEnabled) + if ((Boolean)ruleInfo.isEnabled) { - activeRules.Add((string) ruleInfo.name); + activeRules.Add((string)ruleInfo.name); } } editorSession.AnalysisService.ActiveRules = activeRules.ToArray(); @@ -306,8 +307,9 @@ private async Task HandleScriptFileMarkersRequest( { var markers = await editorSession.AnalysisService.GetSemanticMarkersAsync( editorSession.Workspace.GetFile(requestParams.fileUri), - editorSession.AnalysisService.GetPSSASettingsHashtable(requestParams.settings)); - await requestContext.SendResult(new ScriptFileMarkerRequestResultParams { + AnalysisService.GetPSSASettingsHashtable(requestParams.settings)); + await requestContext.SendResult(new ScriptFileMarkerRequestResultParams + { markers = markers }); } @@ -569,7 +571,7 @@ protected async Task HandleDidChangeConfigurationNotification( { bool oldLoadProfiles = this.currentSettings.EnableProfileLoading; bool oldScriptAnalysisEnabled = - this.currentSettings.ScriptAnalysis.Enable.HasValue ? this.currentSettings.ScriptAnalysis.Enable.Value : false ; + this.currentSettings.ScriptAnalysis.Enable.HasValue ? this.currentSettings.ScriptAnalysis.Enable.Value : false; string oldScriptAnalysisSettingsPath = this.currentSettings.ScriptAnalysis.SettingsPath; @@ -1072,6 +1074,42 @@ protected async Task HandleGetPSHostProcessesRequest( await requestContext.SendResult(psHostProcesses.ToArray()); } + protected async Task HandleCommentHelpRequest( + CommentHelpRequestParams requestParams, + RequestContext requestContext) + { + var scriptFile = EditorSession.Workspace.GetFile(requestParams.DocumentUri); + var expectedFunctionLine = requestParams.TriggerPosition.Line + 2; + var functionDefinitionAst = EditorSession.LanguageService.GetFunctionDefinitionAtLine( + scriptFile, + expectedFunctionLine); + var result = new CommentHelpRequestResult(); + + if (functionDefinitionAst != null) + { + // todo create a semantic marker api that take only string + var analysisResults = await EditorSession.AnalysisService.GetSemanticMarkersAsync( + scriptFile, + AnalysisService.GetCommentHelpRuleSettings( + true, + false, + requestParams.BlockComment, + true, + "before")); + + var analysisResult = analysisResults?.FirstOrDefault(x => + { + return x.Correction != null + && x.Correction.Edits[0].StartLineNumber == expectedFunctionLine; + }); + + // find the analysis result whose correction starts on + result.Content = analysisResult?.Correction.Edits[0].Text.Split('\n').Select(x => x.Trim('\r')).ToArray(); + } + + await requestContext.SendResult(result); + } + private bool IsQueryMatch(string query, string symbolName) { return symbolName.IndexOf(query, StringComparison.OrdinalIgnoreCase) >= 0; @@ -1134,12 +1172,12 @@ protected Task HandleEvaluateRequest( // 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 - }); + requestContext.SendResult( + new DebugAdapterMessages.EvaluateResponseBody + { + Result = "", + VariablesReference = 0 + }); }); return Task.FromResult(true); diff --git a/src/PowerShellEditorServices/Analysis/AnalysisService.cs b/src/PowerShellEditorServices/Analysis/AnalysisService.cs index 5aa9e1a9f..f5987d888 100644 --- a/src/PowerShellEditorServices/Analysis/AnalysisService.cs +++ b/src/PowerShellEditorServices/Analysis/AnalysisService.cs @@ -55,7 +55,7 @@ private bool hasScriptAnalyzerModule "PSShouldProcess", "PSMissingModuleManifestField", "PSAvoidDefaultValueSwitchParameter", - "PSUseDeclaredVarsMoreThanAssigments" + "PSUseDeclaredVarsMoreThanAssignments" }; #endregion // Private Fields @@ -141,6 +141,54 @@ public AnalysisService(IConsoleHost consoleHost, string settingsPath = null) #region Public Methods + /// + /// Get PSScriptAnalyzer settings hashtable for PSProvideCommentHelp rule. + /// + /// Enable the rule. + /// Analyze only exported functions/cmdlets. + /// Use block comment or line comment. + /// Return a vscode snipped correction should be returned. + /// Place comment help at the given location relative to the function definition. + /// A PSScriptAnalyzer settings hashtable. + public static Hashtable GetCommentHelpRuleSettings( + bool enable, + bool exportedOnly, + bool blockComment, + bool vscodeSnippetCorrection, + string placement) + { + var settings = new Dictionary(); + var ruleSettings = new Hashtable(); + ruleSettings.Add("Enable", enable); + ruleSettings.Add("ExportedOnly", exportedOnly); + ruleSettings.Add("BlockComment", blockComment); + ruleSettings.Add("VSCodeSnippetCorrection", vscodeSnippetCorrection); + ruleSettings.Add("Placement", placement); + settings.Add("PSProvideCommentHelp", ruleSettings); + return GetPSSASettingsHashtable(settings); + } + + /// + /// Construct a PSScriptAnalyzer settings hashtable + /// + /// A settings hashtable + /// + public static Hashtable GetPSSASettingsHashtable(IDictionary ruleSettingsMap) + { + var hashtable = new Hashtable(); + var ruleSettingsHashtable = new Hashtable(); + + hashtable["IncludeRules"] = ruleSettingsMap.Keys.ToArray(); + hashtable["Rules"] = ruleSettingsHashtable; + + foreach (var kvp in ruleSettingsMap) + { + ruleSettingsHashtable.Add(kvp.Key, kvp.Value); + } + + return hashtable; + } + /// /// Perform semantic analysis on the given ScriptFile and returns /// an array of ScriptFileMarkers. @@ -181,27 +229,6 @@ public IEnumerable GetPSScriptAnalyzerRules() return ruleNames; } - /// - /// Construct a PSScriptAnalyzer settings hashtable - /// - /// A settings hashtable - /// - public Hashtable GetPSSASettingsHashtable(IDictionary ruleSettingsMap) - { - var hashtable = new Hashtable(); - var ruleSettingsHashtable = new Hashtable(); - - hashtable["IncludeRules"] = ruleSettingsMap.Keys.ToArray(); - hashtable["Rules"] = ruleSettingsHashtable; - - foreach (var kvp in ruleSettingsMap) - { - ruleSettingsHashtable.Add(kvp.Key, kvp.Value); - } - - return hashtable; - } - /// /// Disposes the runspace being used by the analysis service. /// diff --git a/src/PowerShellEditorServices/Language/LanguageService.cs b/src/PowerShellEditorServices/Language/LanguageService.cs index 511110e5c..3a953f5f5 100644 --- a/src/PowerShellEditorServices/Language/LanguageService.cs +++ b/src/PowerShellEditorServices/Language/LanguageService.cs @@ -115,7 +115,7 @@ await AstOperations.GetCompletions( return completionResults; } - catch(ArgumentException e) + catch (ArgumentException e) { // Bad completion results could return an invalid // replacement range, catch that here @@ -231,7 +231,8 @@ public FindOccurrencesResult FindSymbolsInFile(ScriptFile scriptFile) AstOperations .FindSymbolsInDocument(scriptFile.ScriptAst, this.powerShellContext.LocalPowerShellVersion.Version) .Select( - reference => { + reference => + { reference.SourceLine = scriptFile.GetLine(reference.ScriptRegion.StartLineNumber); reference.FilePath = scriptFile.FilePath; @@ -239,7 +240,8 @@ public FindOccurrencesResult FindSymbolsInFile(ScriptFile scriptFile) }); return - new FindOccurrencesResult { + new FindOccurrencesResult + { FoundOccurrences = symbolReferencesinFile }; } @@ -267,7 +269,7 @@ public async Task FindReferencesOfSymbol( // We want to look for references first in referenced files, hence we use ordered dictionary var fileMap = new OrderedDictionary(StringComparer.OrdinalIgnoreCase); - foreach(ScriptFile file in referencedFiles) + foreach (ScriptFile file in referencedFiles) { fileMap.Add(file.FilePath, file); } @@ -520,6 +522,23 @@ public ScriptRegion FindSmallestStatementAstRegion( return ScriptRegion.Create(ast.Extent); } + /// + /// Gets the function defined on a given line. + /// + /// Open script file. + /// The 1 based line on which to look for function definition. + /// If found, returns the function definition on the given line. Otherwise, returns null. + public FunctionDefinitionAst GetFunctionDefinitionAtLine( + ScriptFile scriptFile, + int lineNumber) + { + var functionDefinitionAst = scriptFile.ScriptAst.Find( + ast => ast is FunctionDefinitionAst && ast.Extent.StartLineNumber == lineNumber, + true); + + return functionDefinitionAst as FunctionDefinitionAst; + } + #endregion #region Private Fields