diff --git a/src/PowerShellEditorServices.Protocol/LanguageServer/Formatting.cs b/src/PowerShellEditorServices.Protocol/LanguageServer/Formatting.cs new file mode 100644 index 000000000..f063ba163 --- /dev/null +++ b/src/PowerShellEditorServices.Protocol/LanguageServer/Formatting.cs @@ -0,0 +1,95 @@ +// +// 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 +{ + public class DocumentFormattingRequest + { + public static readonly RequestType Type = RequestType.Create("textDocument/formatting"); + } + + public class DocumentRangeFormattingRequest + { + public static readonly RequestType Type = RequestType.Create("textDocument/rangeFormatting"); + + } + + public class DocumentOnTypeFormattingRequest + { + public static readonly RequestType Type = RequestType.Create("textDocument/onTypeFormatting"); + + } + + public class DocumentRangeFormattingParams + { + /// + /// The document to format. + /// + public TextDocumentIdentifier TextDocument { get; set; } + + /// + /// The range to format. + /// + /// + public Range Range { get; set; } + + /// + /// The format options. + /// + public FormattingOptions Options { get; set; } + } + + public class DocumentOnTypeFormattingParams + { + /// + /// The document to format. + /// + public TextDocumentIdentifier TextDocument { get; set; } + + /// + /// The position at which this request was sent. + /// + public Position Position { get; set; } + + /// + /// The character that has been typed. + /// + public string ch { get; set; } + + /// + /// The format options. + /// + public FormattingOptions options { get; set; } + } + + public class DocumentFormattingParams + { + /// + /// The document to format. + /// + public TextDocumentIdentifier TextDocument { get; set; } + + /// + /// The format options. + /// + public FormattingOptions options { get; set; } + } + + public class FormattingOptions + { + /// + /// Size of a tab in spaces. + /// + public int TabSize { get; set; } + + /// + /// Prefer spaces over tabs. + /// + public bool InsertSpaces { get; set; } + } +} + diff --git a/src/PowerShellEditorServices.Protocol/LanguageServer/Hover.cs b/src/PowerShellEditorServices.Protocol/LanguageServer/Hover.cs index 5a036bd7b..42c853254 100644 --- a/src/PowerShellEditorServices.Protocol/LanguageServer/Hover.cs +++ b/src/PowerShellEditorServices.Protocol/LanguageServer/Hover.cs @@ -23,7 +23,7 @@ public class Hover { public MarkedString[] Contents { get; set; } - public Range? Range { get; set; } + public Range Range { get; set; } } public class HoverRequest diff --git a/src/PowerShellEditorServices.Protocol/LanguageServer/ScriptFileMarkersRequest.cs b/src/PowerShellEditorServices.Protocol/LanguageServer/ScriptFileMarkersRequest.cs deleted file mode 100644 index 5394ddd4c..000000000 --- a/src/PowerShellEditorServices.Protocol/LanguageServer/ScriptFileMarkersRequest.cs +++ /dev/null @@ -1,54 +0,0 @@ -// -// 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; -using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; -using System.Collections.Generic; - -namespace Microsoft.PowerShell.EditorServices.Protocol.LanguageServer -{ - /// - /// Class to encapsulate the request type. - /// - class ScriptFileMarkersRequest - { - public static readonly - RequestType Type = - RequestType.Create("powerShell/getScriptFileMarkers"); - } - - /// - /// Class to encapsulate the request parameters. - /// - class ScriptFileMarkerRequestParams - { - /// - /// Path of the file for which the markers are requested. - /// - public string fileUri; - - /// - /// Settings to be provided to ScriptAnalyzer to get the markers. - /// - /// We have this unusual structure because JSON deserializer - /// does not deserialize nested hashtables. i.e. it won't - /// deserialize a hashtable within a hashtable. But in this case, - /// i.e. a hashtable within a dictionary, it will deserialize - /// the hashtable. - /// - public Dictionary settings; - } - - /// - /// Class to encapsulate the result of marker request. - /// - class ScriptFileMarkerRequestResultParams - { - /// - /// An array of markers obtained by analyzing the given file. - /// - public ScriptFileMarker[] markers; - } -} diff --git a/src/PowerShellEditorServices.Protocol/LanguageServer/TextDocument.cs b/src/PowerShellEditorServices.Protocol/LanguageServer/TextDocument.cs index 13cbdb22d..edd66f41a 100644 --- a/src/PowerShellEditorServices.Protocol/LanguageServer/TextDocument.cs +++ b/src/PowerShellEditorServices.Protocol/LanguageServer/TextDocument.cs @@ -237,7 +237,7 @@ public class TextDocumentChangeEvent /// Gets or sets the Range where the document was changed. Will /// be null if the server's TextDocumentSyncKind is Full. /// - public Range? Range { get; set; } + public Range Range { get; set; } /// /// Gets or sets the length of the Range being replaced in the @@ -267,7 +267,7 @@ public class Position } [DebuggerDisplay("Start = {Start.Line}:{Start.Character}, End = {End.Line}:{End.Character}")] - public struct Range + public class Range { /// /// Gets or sets the starting position of the range. diff --git a/src/PowerShellEditorServices.Protocol/Server/LanguageServer.cs b/src/PowerShellEditorServices.Protocol/Server/LanguageServer.cs index b611395bc..897299631 100644 --- a/src/PowerShellEditorServices.Protocol/Server/LanguageServer.cs +++ b/src/PowerShellEditorServices.Protocol/Server/LanguageServer.cs @@ -104,6 +104,10 @@ public void Start() 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(ExpandAliasRequest.Type, this.HandleExpandAliasRequest); @@ -123,7 +127,6 @@ public void Start() this.messageHandlers.SetRequestHandler(GetPSSARulesRequest.Type, this.HandleGetPSSARulesRequest); this.messageHandlers.SetRequestHandler(SetPSSARulesRequest.Type, this.HandleSetPSSARulesRequest); - this.messageHandlers.SetRequestHandler(ScriptFileMarkersRequest.Type, this.HandleScriptFileMarkersRequest); this.messageHandlers.SetRequestHandler(ScriptRegionRequest.Type, this.HandleGetFormatScriptRegionRequest); this.messageHandlers.SetRequestHandler(GetPSHostProcessesRequest.Type, this.HandleGetPSHostProcessesRequest); @@ -200,7 +203,8 @@ await requestContext.SendResult( SignatureHelpProvider = new SignatureHelpOptions { TriggerCharacters = new string[] { " " } // TODO: Other characters here? - } + }, + DocumentFormattingProvider = false } }); } @@ -292,19 +296,6 @@ await requestContext.SendResult(new ScriptRegionRequestResult }); } - private async Task HandleScriptFileMarkersRequest( - ScriptFileMarkerRequestParams requestParams, - RequestContext requestContext) - { - var markers = await editorSession.AnalysisService.GetSemanticMarkersAsync( - editorSession.Workspace.GetFile(requestParams.fileUri), - AnalysisService.GetPSSASettingsHashtable(requestParams.settings)); - await requestContext.SendResult(new ScriptFileMarkerRequestResultParams - { - markers = markers - }); - } - private async Task HandleGetPSSARulesRequest( object param, RequestContext requestContext) @@ -541,7 +532,7 @@ protected Task HandleDidChangeTextDocumentNotification( changedFile.ApplyChange( GetFileChangeDetails( - textChange.Range.Value, + textChange.Range, textChange.Text)); changedFiles.Add(changedFile); @@ -873,7 +864,7 @@ await editorSession textDocumentPositionParams.Position.Character + 1); List symbolInfo = new List(); - Range? symbolRange = null; + Range symbolRange = null; if (symbolDetails != null) { @@ -1155,7 +1146,45 @@ await requestContext.SendResult( codeActionCommands.ToArray()); } - protected Task HandleEvaluateRequest( + protected async Task HandleDocumentFormattingRequest( + DocumentFormattingParams formattingParams, + RequestContext 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 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 requestContext) { @@ -1193,6 +1222,49 @@ protected Task HandleEvaluateRequest( #region Event Handlers + private async Task> 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( diff --git a/src/PowerShellEditorServices.Protocol/Server/LanguageServerSettings.cs b/src/PowerShellEditorServices.Protocol/Server/LanguageServerSettings.cs index 55879e8a3..7ab833546 100644 --- a/src/PowerShellEditorServices.Protocol/Server/LanguageServerSettings.cs +++ b/src/PowerShellEditorServices.Protocol/Server/LanguageServerSettings.cs @@ -5,6 +5,9 @@ using System.IO; using Microsoft.PowerShell.EditorServices.Utility; +using System; +using System.Reflection; +using System.Collections; namespace Microsoft.PowerShell.EditorServices.Protocol.Server { @@ -14,9 +17,12 @@ public class LanguageServerSettings public ScriptAnalysisSettings ScriptAnalysis { get; set; } + public CodeFormattingSettings CodeFormatting { get; set; } + public LanguageServerSettings() { this.ScriptAnalysis = new ScriptAnalysisSettings(); + this.CodeFormatting = new CodeFormattingSettings(); } public void Update( @@ -31,6 +37,7 @@ public void Update( settings.ScriptAnalysis, workspaceRootPath, logger); + this.CodeFormatting = new CodeFormattingSettings(settings.CodeFormatting); } } } @@ -86,12 +93,92 @@ public void Update( } } - public class LanguageServerSettingsWrapper + public class CodeFormattingSettings { - // NOTE: This property is capitalized as 'Powershell' because the - // mode name sent from the client is written as 'powershell' and - // JSON.net is using camelCasing. + /// + /// Default constructor. + /// + public CodeFormattingSettings() + { + + } + + /// + /// Copy constructor. + /// + /// An instance of type CodeFormattingSettings. + public CodeFormattingSettings(CodeFormattingSettings codeFormattingSettings) + { + if (codeFormattingSettings == null) + { + throw new ArgumentNullException(nameof(codeFormattingSettings)); + } - public LanguageServerSettings Powershell { get; set; } + foreach (var prop in this.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance)) + { + prop.SetValue(this, prop.GetValue(codeFormattingSettings)); + } + } + + public bool OpenBraceOnSameLine { get; set; } + public bool NewLineAfterOpenBrace { get; set; } + public bool NewLineAfterCloseBrace { get; set; } + public bool WhitespaceBeforeOpenBrace { get; set; } + public bool WhitespaceBeforeOpenParen { get; set; } + public bool WhitespaceAroundOperator { get; set; } + public bool WhitespaceAfterSeparator { get; set; } + public bool IgnoreOneLineBlock { get; set; } + public bool AlignPropertyValuePairs { get; set; } + + public Hashtable GetPSSASettingsHashTable(int tabSize, bool insertSpaces) + { + return new Hashtable + { + {"IncludeRules", new string[] { + "PSPlaceCloseBrace", + "PSPlaceOpenBrace", + "PSUseConsistentWhitespace", + "PSUseConsistentIndentation", + "PSAlignAssignmentStatement" + }}, + {"Rules", new Hashtable { + {"PSPlaceOpenBrace", new Hashtable { + {"Enable", true}, + {"OnSameLine", OpenBraceOnSameLine}, + {"NewLineAfter", NewLineAfterOpenBrace}, + {"IgnoreOneLineBlock", IgnoreOneLineBlock} + }}, + {"PSPlaceCloseBrace", new Hashtable { + {"Enable", true}, + {"NewLineAfter", NewLineAfterCloseBrace}, + {"IgnoreOneLineBlock", IgnoreOneLineBlock} + }}, + {"PSUseConsistentIndentation", new Hashtable { + {"Enable", true}, + {"IndentationSize", tabSize} + }}, + {"PSUseConsistentWhitespace", new Hashtable { + {"Enable", true}, + {"CheckOpenBrace", WhitespaceBeforeOpenBrace}, + {"CheckOpenParen", WhitespaceBeforeOpenParen}, + {"CheckOperator", WhitespaceAroundOperator}, + {"CheckSeparator", WhitespaceAfterSeparator} + }}, + {"PSAlignAssignmentStatement", new Hashtable { + {"Enable", true}, + {"CheckHashtable", AlignPropertyValuePairs} + }}, + }} + }; + } + } + + public class LanguageServerSettingsWrapper + { + // NOTE: This property is capitalized as 'Powershell' because the + // mode name sent from the client is written as 'powershell' and + // JSON.net is using camelCasing. + + public LanguageServerSettings Powershell { get; set; } + } } -} diff --git a/src/PowerShellEditorServices/Analysis/AnalysisService.cs b/src/PowerShellEditorServices/Analysis/AnalysisService.cs index ca6e98f12..109de63b9 100644 --- a/src/PowerShellEditorServices/Analysis/AnalysisService.cs +++ b/src/PowerShellEditorServices/Analysis/AnalysisService.cs @@ -129,6 +129,7 @@ public AnalysisService(string settingsPath, ILogger logger) this.analysisRunspacePool.Open(); ActiveRules = IncludedRules.ToArray(); + EnumeratePSScriptAnalyzerCmdlets(); EnumeratePSScriptAnalyzerRules(); } catch (Exception e) @@ -245,6 +246,37 @@ public IEnumerable GetPSScriptAnalyzerRules() return ruleNames; } + /// + /// Format a given script text with default codeformatting settings. + /// + /// Script text to be formatted + /// ScriptAnalyzer settings + /// The range within which formatting should be applied. + /// The formatted script text. + public async Task Format( + string scriptDefinition, + Hashtable settings, + int[] rangeList) + { + // we cannot use Range type therefore this workaround of using -1 default value + if (!hasScriptAnalyzerModule) + { + return null; + } + + var argsDict = new Dictionary { + {"ScriptDefinition", scriptDefinition}, + {"Settings", settings} + }; + if (rangeList != null) + { + argsDict.Add("Range", rangeList); + } + + var result = await InvokePowerShellAsync("Invoke-Formatter", argsDict); + return result?.Select(r => r?.ImmediateBaseObject as string).FirstOrDefault(); + } + /// /// Disposes the runspace being used by the analysis service. /// @@ -335,6 +367,29 @@ private static PSModuleInfo FindPSScriptAnalyzerModule(ILogger logger) } } + private void EnumeratePSScriptAnalyzerCmdlets() + { + if (hasScriptAnalyzerModule) + { + var sb = new StringBuilder(); + var commands = InvokePowerShell( + "Get-Command", + new Dictionary + { + {"Module", "PSScriptAnalyzer"} + }); + + var commandNames = commands? + .Select(c => c.ImmediateBaseObject as CmdletInfo) + .Where(c => c != null) + .Select(c => c.Name) ?? Enumerable.Empty(); + + sb.AppendLine("The following cmdlets are available in the imported PSScriptAnalyzer module:"); + sb.AppendLine(String.Join(Environment.NewLine, commandNames.Select(s => " " + s))); + this.logger.Write(LogLevel.Verbose, sb.ToString()); + } + } + private void EnumeratePSScriptAnalyzerRules() { if (hasScriptAnalyzerModule) diff --git a/test/PowerShellEditorServices.Test.Host/LanguageServerTests.cs b/test/PowerShellEditorServices.Test.Host/LanguageServerTests.cs index 6a9880cf9..eeaac47cb 100644 --- a/test/PowerShellEditorServices.Test.Host/LanguageServerTests.cs +++ b/test/PowerShellEditorServices.Test.Host/LanguageServerTests.cs @@ -879,7 +879,8 @@ await this.languageServiceClient.SendEvent( ScriptAnalysis = new ScriptAnalysisSettings { Enable = false - } + }, + CodeFormatting = new CodeFormattingSettings() } } });