Skip to content

Commit a2bb4ad

Browse files
authored
Merge pull request #304 from PowerShell/kapilmb/code-actions
Provide quickfix functionality
2 parents c42d3c2 + 24a34c1 commit a2bb4ad

File tree

6 files changed

+175
-28
lines changed

6 files changed

+175
-28
lines changed
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol;
2+
using Newtonsoft.Json.Linq;
3+
4+
namespace Microsoft.PowerShell.EditorServices.Protocol.LanguageServer
5+
{
6+
public class CodeActionRequest
7+
{
8+
public static readonly
9+
RequestType<CodeActionRequest, CodeActionCommand[]> Type =
10+
RequestType<CodeActionRequest, CodeActionCommand[]>.Create("textDocument/codeAction");
11+
12+
public TextDocumentIdentifier TextDocument { get; set; }
13+
14+
public Range Range { get; set; }
15+
16+
public CodeActionContext Context { get; set; }
17+
}
18+
19+
public class CodeActionContext
20+
{
21+
public Diagnostic[] Diagnostics { get; set; }
22+
}
23+
24+
public class CodeActionCommand
25+
{
26+
public string Title { get; set; }
27+
28+
public string Command { get; set; }
29+
30+
public JArray Arguments { get; set; }
31+
}
32+
}

src/PowerShellEditorServices.Protocol/LanguageServer/ServerCapabilities.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ public class ServerCapabilities
2424
public bool? DocumentSymbolProvider { get; set; }
2525

2626
public bool? WorkspaceSymbolProvider { get; set; }
27+
28+
public bool? CodeActionProvider { get; set; }
2729
}
2830

2931
/// <summary>

src/PowerShellEditorServices.Protocol/PowerShellEditorServices.Protocol.csproj

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
<Compile Include="DebugAdapter\SetFunctionBreakpointsRequest.cs" />
5757
<Compile Include="DebugAdapter\SetVariableRequest.cs" />
5858
<Compile Include="LanguageServer\EditorCommands.cs" />
59+
<Compile Include="LanguageServer\CodeAction.cs" />
5960
<Compile Include="LanguageServer\FindModuleRequest.cs" />
6061
<Compile Include="LanguageServer\InstallModuleRequest.cs" />
6162
<Compile Include="LanguageServer\PowerShellVersionRequest.cs" />
@@ -155,7 +156,7 @@
155156
</PropertyGroup>
156157
<Error Condition="!Exists('$(SolutionDir)\.nuget\NuGet.targets')" Text="$([System.String]::Format('$(ErrorText)', '$(SolutionDir)\.nuget\NuGet.targets'))" />
157158
</Target>
158-
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
159+
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
159160
Other similar extension points exist, see Microsoft.Common.targets.
160161
<Target Name="BeforeBuild">
161162
</Target>

src/PowerShellEditorServices.Protocol/Server/LanguageServer.cs

Lines changed: 69 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Channel;
1010
using Microsoft.PowerShell.EditorServices.Session;
1111
using Microsoft.PowerShell.EditorServices.Utility;
12+
using Newtonsoft.Json.Linq;
1213
using System;
1314
using System.Collections.Generic;
1415
using System.IO;
@@ -30,6 +31,8 @@ public class LanguageServer : LanguageServerBase
3031
private OutputDebouncer outputDebouncer;
3132
private LanguageServerEditorOperations editorOperations;
3233
private LanguageServerSettings currentSettings = new LanguageServerSettings();
34+
private Dictionary<string, Dictionary<string, MarkerCorrection>> codeActionsPerFile =
35+
new Dictionary<string, Dictionary<string, MarkerCorrection>>();
3336

3437
/// <param name="hostDetails">
3538
/// Provides details about the host application.
@@ -92,6 +95,7 @@ protected override void Initialize()
9295
this.SetRequestHandler(HoverRequest.Type, this.HandleHoverRequest);
9396
this.SetRequestHandler(DocumentSymbolRequest.Type, this.HandleDocumentSymbolRequest);
9497
this.SetRequestHandler(WorkspaceSymbolRequest.Type, this.HandleWorkspaceSymbolRequest);
98+
this.SetRequestHandler(CodeActionRequest.Type, this.HandleCodeActionRequest);
9599

96100
this.SetRequestHandler(ShowOnlineHelpRequest.Type, this.HandleShowOnlineHelpRequest);
97101
this.SetRequestHandler(ExpandAliasRequest.Type, this.HandleExpandAliasRequest);
@@ -146,6 +150,7 @@ await requestContext.SendResult(
146150
DocumentSymbolProvider = true,
147151
WorkspaceSymbolProvider = true,
148152
HoverProvider = true,
153+
CodeActionProvider = true,
149154
CompletionProvider = new CompletionOptions
150155
{
151156
ResolveProvider = true,
@@ -226,17 +231,17 @@ function __Expand-Alias {
226231
param($targetScript)
227232
228233
[ref]$errors=$null
229-
230-
$tokens = [System.Management.Automation.PsParser]::Tokenize($targetScript, $errors).Where({$_.type -eq 'command'}) |
234+
235+
$tokens = [System.Management.Automation.PsParser]::Tokenize($targetScript, $errors).Where({$_.type -eq 'command'}) |
231236
Sort Start -Descending
232237
233238
foreach ($token in $tokens) {
234239
$definition=(Get-Command ('`'+$token.Content) -CommandType Alias -ErrorAction SilentlyContinue).Definition
235240
236-
if($definition) {
241+
if($definition) {
237242
$lhs=$targetScript.Substring(0, $token.Start)
238243
$rhs=$targetScript.Substring($token.Start + $token.Length)
239-
244+
240245
$targetScript=$lhs + $definition + $rhs
241246
}
242247
}
@@ -346,13 +351,13 @@ protected async Task HandleDidChangeConfigurationNotification(
346351
EventContext eventContext)
347352
{
348353
bool oldLoadProfiles = this.currentSettings.EnableProfileLoading;
349-
bool oldScriptAnalysisEnabled =
354+
bool oldScriptAnalysisEnabled =
350355
this.currentSettings.ScriptAnalysis.Enable.HasValue;
351356
string oldScriptAnalysisSettingsPath =
352357
this.currentSettings.ScriptAnalysis.SettingsPath;
353358

354359
this.currentSettings.Update(
355-
configChangeParams.Settings.Powershell,
360+
configChangeParams.Settings.Powershell,
356361
this.editorSession.Workspace.WorkspacePath);
357362

358363
if (!this.profilesLoaded &&
@@ -386,6 +391,7 @@ protected async Task HandleDidChangeConfigurationNotification(
386391
await PublishScriptDiagnostics(
387392
scriptFile,
388393
emptyAnalysisDiagnostics,
394+
this.codeActionsPerFile,
389395
eventContext);
390396
}
391397
}
@@ -815,6 +821,35 @@ private bool IsQueryMatch(string query, string symbolName)
815821
return symbolName.IndexOf(query, StringComparison.OrdinalIgnoreCase) >= 0;
816822
}
817823

824+
protected async Task HandleCodeActionRequest(
825+
CodeActionRequest codeActionParams,
826+
RequestContext<CodeActionCommand[]> requestContext)
827+
{
828+
MarkerCorrection correction = null;
829+
Dictionary<string, MarkerCorrection> markerIndex = null;
830+
List<CodeActionCommand> codeActionCommands = new List<CodeActionCommand>();
831+
832+
if (this.codeActionsPerFile.TryGetValue(codeActionParams.TextDocument.Uri, out markerIndex))
833+
{
834+
foreach (var diagnostic in codeActionParams.Context.Diagnostics)
835+
{
836+
if (markerIndex.TryGetValue(diagnostic.Code, out correction))
837+
{
838+
codeActionCommands.Add(
839+
new CodeActionCommand
840+
{
841+
Title = correction.Name,
842+
Command = "PowerShell.ApplyCodeActionEdits",
843+
Arguments = JArray.FromObject(correction.Edits)
844+
});
845+
}
846+
}
847+
}
848+
849+
await requestContext.SendResult(
850+
codeActionCommands.ToArray());
851+
}
852+
818853
protected Task HandleEvaluateRequest(
819854
DebugAdapterMessages.EvaluateRequestArguments evaluateParams,
820855
RequestContext<DebugAdapterMessages.EvaluateResponseBody> requestContext)
@@ -974,6 +1009,7 @@ private Task RunScriptDiagnostics(
9741009
DelayThenInvokeDiagnostics(
9751010
750,
9761011
filesToAnalyze,
1012+
this.codeActionsPerFile,
9771013
editorSession,
9781014
eventContext,
9791015
existingRequestCancellation.Token),
@@ -987,6 +1023,7 @@ private Task RunScriptDiagnostics(
9871023
private static async Task DelayThenInvokeDiagnostics(
9881024
int delayMilliseconds,
9891025
ScriptFile[] filesToAnalyze,
1026+
Dictionary<string, Dictionary<string, MarkerCorrection>> correctionIndex,
9901027
EditorSession editorSession,
9911028
EventContext eventContext,
9921029
CancellationToken cancellationToken)
@@ -1034,28 +1071,45 @@ private static async Task DelayThenInvokeDiagnostics(
10341071
await PublishScriptDiagnostics(
10351072
scriptFile,
10361073
semanticMarkers,
1074+
correctionIndex,
10371075
eventContext);
10381076
}
10391077
}
10401078

10411079
private static async Task PublishScriptDiagnostics(
10421080
ScriptFile scriptFile,
10431081
ScriptFileMarker[] semanticMarkers,
1082+
Dictionary<string, Dictionary<string, MarkerCorrection>> correctionIndex,
10441083
EventContext eventContext)
10451084
{
1046-
var allMarkers = scriptFile.SyntaxMarkers.Concat(semanticMarkers);
1085+
List<Diagnostic> diagnostics = new List<Diagnostic>();
1086+
1087+
// Hold on to any corrections that may need to be applied later
1088+
Dictionary<string, MarkerCorrection> fileCorrections =
1089+
new Dictionary<string, MarkerCorrection>();
1090+
1091+
foreach (var marker in scriptFile.SyntaxMarkers.Concat(semanticMarkers))
1092+
{
1093+
// Does the marker contain a correction?
1094+
Diagnostic markerDiagnostic = GetDiagnosticFromMarker(marker);
1095+
if (marker.Correction != null)
1096+
{
1097+
fileCorrections.Add(markerDiagnostic.Code, marker.Correction);
1098+
}
1099+
1100+
diagnostics.Add(markerDiagnostic);
1101+
}
1102+
1103+
correctionIndex[scriptFile.ClientFilePath] = fileCorrections;
10471104

1048-
// Always send syntax and semantic errors. We want to
1105+
// Always send syntax and semantic errors. We want to
10491106
// make sure no out-of-date markers are being displayed.
10501107
await eventContext.SendEvent(
10511108
PublishDiagnosticsNotification.Type,
10521109
new PublishDiagnosticsNotification
10531110
{
10541111
Uri = scriptFile.ClientFilePath,
1055-
Diagnostics =
1056-
allMarkers
1057-
.Select(GetDiagnosticFromMarker)
1058-
.ToArray()
1112+
Diagnostics = diagnostics.ToArray()
10591113
});
10601114
}
10611115

@@ -1065,6 +1119,7 @@ private static Diagnostic GetDiagnosticFromMarker(ScriptFileMarker scriptFileMar
10651119
{
10661120
Severity = MapDiagnosticSeverity(scriptFileMarker.Level),
10671121
Message = scriptFileMarker.Message,
1122+
Code = Guid.NewGuid().ToString(),
10681123
Range = new Range
10691124
{
10701125
// TODO: What offsets should I use?
@@ -1145,7 +1200,7 @@ private static CompletionItem CreateCompletionItem(
11451200
if (!completionDetails.ListItemText.Equals(
11461201
completionDetails.ToolTipText,
11471202
StringComparison.OrdinalIgnoreCase) &&
1148-
!Regex.IsMatch(completionDetails.ToolTipText,
1203+
!Regex.IsMatch(completionDetails.ToolTipText,
11491204
@"^\s*" + escapedToolTipText + @"\s+\["))
11501205
{
11511206
detailString = completionDetails.ToolTipText;
@@ -1158,7 +1213,7 @@ private static CompletionItem CreateCompletionItem(
11581213
// default (with common params at the end). We just need to make sure the default
11591214
// order also be the lexicographical order which we do by prefixig the ListItemText
11601215
// with a leading 0's four digit index. This would not sort correctly for a list
1161-
// > 999 parameters but surely we won't have so many items in the "parameter name"
1216+
// > 999 parameters but surely we won't have so many items in the "parameter name"
11621217
// completion list. Technically we don't need the ListItemText at all but it may come
11631218
// in handy during debug.
11641219
var sortText = (completionDetails.CompletionType == CompletionType.ParameterName)

src/PowerShellEditorServices/Analysis/AnalysisService.cs

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
namespace Microsoft.PowerShell.EditorServices
1818
{
1919
/// <summary>
20-
/// Provides a high-level service for performing semantic analysis
20+
/// Provides a high-level service for performing semantic analysis
2121
/// of PowerShell scripts.
2222
/// </summary>
2323
public class AnalysisService : IDisposable
@@ -31,15 +31,19 @@ public class AnalysisService : IDisposable
3131
/// Defines the list of Script Analyzer rules to include by default if
3232
/// no settings file is specified.
3333
/// </summary>
34-
private static readonly string[] IncludedRules = {
34+
private static readonly string[] IncludedRules = new string[]
35+
{
36+
"PSUseToExportFieldsInManifest",
37+
"PSMisleadingBacktick",
38+
"PSAvoidUsingCmdletAliases",
3539
"PSUseApprovedVerbs",
40+
"PSAvoidUsingPlainTextForPassword",
3641
"PSReservedCmdletChar",
3742
"PSReservedParams",
3843
"PSShouldProcess",
3944
"PSMissingModuleManifestField",
4045
"PSAvoidDefaultValueSwitchParameter",
41-
"PSUseDeclaredVarsMoreThanAssigments",
42-
"PSMisleadingBacktick",
46+
"PSUseDeclaredVarsMoreThanAssigments"
4347
};
4448

4549
#endregion // Private Fields
@@ -64,7 +68,7 @@ public string SettingsPath
6468
/// <summary>
6569
/// Creates an instance of the AnalysisService class.
6670
/// </summary>
67-
/// <param name="consoleHost">An object that implements IConsoleHost in which to write errors/warnings
71+
/// <param name="consoleHost">An object that implements IConsoleHost in which to write errors/warnings
6872
/// from analyzer.</param>
6973
/// <param name="settingsPath">Path to a PSScriptAnalyzer settings file.</param>
7074
public AnalysisService(IConsoleHost consoleHost, string settingsPath = null)
@@ -107,8 +111,8 @@ public ScriptFileMarker[] GetSemanticMarkers(ScriptFile file)
107111
Task.Factory.StartNew<ScriptFileMarker[]>(
108112
() =>
109113
{
110-
return
111-
GetDiagnosticRecords(file)
114+
return
115+
GetDiagnosticRecords(file)
112116
.Select(ScriptFileMarker.FromDiagnosticRecord)
113117
.ToArray();
114118
},

0 commit comments

Comments
 (0)