1717using System . Linq ;
1818using System . Management . Automation ;
1919using System . Management . Automation . Language ;
20+ using System . Text ;
2021using System . Text . RegularExpressions ;
2122using System . Threading ;
2223using System . Threading . Tasks ;
@@ -27,7 +28,7 @@ namespace Microsoft.PowerShell.EditorServices.Protocol.Server
2728{
2829 public class LanguageServer
2930 {
30- private static CancellationTokenSource existingRequestCancellation ;
31+ private static CancellationTokenSource s_existingRequestCancellation ;
3132
3233 private static readonly Location [ ] s_emptyLocationResult = new Location [ 0 ] ;
3334
@@ -48,6 +49,7 @@ public class LanguageServer
4849 private LanguageServerEditorOperations editorOperations ;
4950 private LanguageServerSettings currentSettings = new LanguageServerSettings ( ) ;
5051
52+ // The outer key is the file's uri, the inner key is a unique id for the diagnostic
5153 private Dictionary < string , Dictionary < string , MarkerCorrection > > codeActionsPerFile =
5254 new Dictionary < string , Dictionary < string , MarkerCorrection > > ( ) ;
5355
@@ -1182,6 +1184,7 @@ private bool IsQueryMatch(string query, string symbolName)
11821184 return symbolName . IndexOf ( query , StringComparison . OrdinalIgnoreCase ) >= 0 ;
11831185 }
11841186
1187+ // https://microsoft.github.io/language-server-protocol/specification#textDocument_codeAction
11851188 protected async Task HandleCodeActionRequest (
11861189 CodeActionParams codeActionParams ,
11871190 RequestContext < CodeActionCommand [ ] > requestContext )
@@ -1190,12 +1193,22 @@ protected async Task HandleCodeActionRequest(
11901193 Dictionary < string , MarkerCorrection > markerIndex = null ;
11911194 List < CodeActionCommand > codeActionCommands = new List < CodeActionCommand > ( ) ;
11921195
1196+ // If there are any code fixes, send these commands first so they appear at top of "Code Fix" menu in the client UI.
11931197 if ( this . codeActionsPerFile . TryGetValue ( codeActionParams . TextDocument . Uri , out markerIndex ) )
11941198 {
11951199 foreach ( var diagnostic in codeActionParams . Context . Diagnostics )
11961200 {
1197- if ( ! string . IsNullOrEmpty ( diagnostic . Code ) &&
1198- markerIndex . TryGetValue ( diagnostic . Code , out correction ) )
1201+ if ( string . IsNullOrEmpty ( diagnostic . Code ) )
1202+ {
1203+ this . Logger . Write (
1204+ LogLevel . Warning ,
1205+ $ "textDocument/codeAction skipping diagnostic with empty Code field: { diagnostic . Source } { diagnostic . Message } ") ;
1206+
1207+ continue ;
1208+ }
1209+
1210+ string diagnosticId = GetUniqueIdFromDiagnostic ( diagnostic ) ;
1211+ if ( markerIndex . TryGetValue ( diagnosticId , out correction ) )
11991212 {
12001213 codeActionCommands . Add (
12011214 new CodeActionCommand
@@ -1208,8 +1221,30 @@ protected async Task HandleCodeActionRequest(
12081221 }
12091222 }
12101223
1211- await requestContext . SendResult (
1212- codeActionCommands . ToArray ( ) ) ;
1224+ // Add "show documentation" commands last so they appear at the bottom of the client UI.
1225+ // These commands do not require code fixes. Sometimes we get a batch of diagnostics
1226+ // to create commands for. No need to create multiple show doc commands for the same rule.
1227+ var ruleNamesProcessed = new HashSet < string > ( ) ;
1228+ foreach ( var diagnostic in codeActionParams . Context . Diagnostics )
1229+ {
1230+ if ( string . IsNullOrEmpty ( diagnostic . Code ) ) { continue ; }
1231+
1232+ if ( string . Equals ( diagnostic . Source , "PSScriptAnalyzer" , StringComparison . OrdinalIgnoreCase ) &&
1233+ ! ruleNamesProcessed . Contains ( diagnostic . Code ) )
1234+ {
1235+ ruleNamesProcessed . Add ( diagnostic . Code ) ;
1236+
1237+ codeActionCommands . Add (
1238+ new CodeActionCommand
1239+ {
1240+ Title = $ "Show documentation for \" { diagnostic . Code } \" ",
1241+ Command = "PowerShell.ShowCodeActionDocumentation" ,
1242+ Arguments = JArray . FromObject ( new [ ] { diagnostic . Code } )
1243+ } ) ;
1244+ }
1245+ }
1246+
1247+ await requestContext . SendResult ( codeActionCommands . ToArray ( ) ) ;
12131248 }
12141249
12151250 protected async Task HandleDocumentFormattingRequest (
@@ -1454,15 +1489,15 @@ private Task RunScriptDiagnostics(
14541489 // If there's an existing task, attempt to cancel it
14551490 try
14561491 {
1457- if ( existingRequestCancellation != null )
1492+ if ( s_existingRequestCancellation != null )
14581493 {
14591494 // Try to cancel the request
1460- existingRequestCancellation . Cancel ( ) ;
1495+ s_existingRequestCancellation . Cancel ( ) ;
14611496
14621497 // If cancellation didn't throw an exception,
14631498 // clean up the existing token
1464- existingRequestCancellation . Dispose ( ) ;
1465- existingRequestCancellation = null ;
1499+ s_existingRequestCancellation . Dispose ( ) ;
1500+ s_existingRequestCancellation = null ;
14661501 }
14671502 }
14681503 catch ( Exception e )
@@ -1479,11 +1514,17 @@ private Task RunScriptDiagnostics(
14791514 return cancelTask . Task ;
14801515 }
14811516
1517+ // If filesToAnalzye is empty, nothing to do so return early.
1518+ if ( filesToAnalyze . Length == 0 )
1519+ {
1520+ return Task . FromResult ( true ) ;
1521+ }
1522+
14821523 // Create a fresh cancellation token and then start the task.
14831524 // We create this on a different TaskScheduler so that we
14841525 // don't block the main message loop thread.
14851526 // TODO: Is there a better way to do this?
1486- existingRequestCancellation = new CancellationTokenSource ( ) ;
1527+ s_existingRequestCancellation = new CancellationTokenSource ( ) ;
14871528 Task . Factory . StartNew (
14881529 ( ) =>
14891530 DelayThenInvokeDiagnostics (
@@ -1494,36 +1535,14 @@ private Task RunScriptDiagnostics(
14941535 editorSession ,
14951536 eventSender ,
14961537 this . Logger ,
1497- existingRequestCancellation . Token ) ,
1538+ s_existingRequestCancellation . Token ) ,
14981539 CancellationToken . None ,
14991540 TaskCreationOptions . None ,
15001541 TaskScheduler . Default ) ;
15011542
15021543 return Task . FromResult ( true ) ;
15031544 }
15041545
1505- private static async Task DelayThenInvokeDiagnostics (
1506- int delayMilliseconds ,
1507- ScriptFile [ ] filesToAnalyze ,
1508- bool isScriptAnalysisEnabled ,
1509- Dictionary < string , Dictionary < string , MarkerCorrection > > correctionIndex ,
1510- EditorSession editorSession ,
1511- EventContext eventContext ,
1512- ILogger Logger ,
1513- CancellationToken cancellationToken )
1514- {
1515- await DelayThenInvokeDiagnostics (
1516- delayMilliseconds ,
1517- filesToAnalyze ,
1518- isScriptAnalysisEnabled ,
1519- correctionIndex ,
1520- editorSession ,
1521- eventContext . SendEvent ,
1522- Logger ,
1523- cancellationToken ) ;
1524- }
1525-
1526-
15271546 private static async Task DelayThenInvokeDiagnostics (
15281547 int delayMilliseconds ,
15291548 ScriptFile [ ] filesToAnalyze ,
@@ -1573,6 +1592,7 @@ private static async Task DelayThenInvokeDiagnostics(
15731592
15741593 await PublishScriptDiagnostics (
15751594 scriptFile ,
1595+ // Concat script analysis errors to any existing parse errors
15761596 scriptFile . SyntaxMarkers . Concat ( semanticMarkers ) . ToArray ( ) ,
15771597 correctionIndex ,
15781598 eventSender ) ;
@@ -1620,7 +1640,8 @@ private static async Task PublishScriptDiagnostics(
16201640 Diagnostic markerDiagnostic = GetDiagnosticFromMarker ( marker ) ;
16211641 if ( marker . Correction != null )
16221642 {
1623- fileCorrections . Add ( markerDiagnostic . Code , marker . Correction ) ;
1643+ string diagnosticId = GetUniqueIdFromDiagnostic ( markerDiagnostic ) ;
1644+ fileCorrections . Add ( diagnosticId , marker . Correction ) ;
16241645 }
16251646
16261647 diagnostics . Add ( markerDiagnostic ) ;
@@ -1639,13 +1660,39 @@ await eventSender(
16391660 } ) ;
16401661 }
16411662
1663+ // Generate a unique id that is used as a key to look up the associated code action (code fix) when
1664+ // we receive and process the textDocument/codeAction message.
1665+ private static string GetUniqueIdFromDiagnostic ( Diagnostic diagnostic )
1666+ {
1667+ Position start = diagnostic . Range . Start ;
1668+ Position end = diagnostic . Range . End ;
1669+
1670+ var sb = new StringBuilder ( 256 )
1671+ . Append ( diagnostic . Source ?? "?" )
1672+ . Append ( "_" )
1673+ . Append ( diagnostic . Code ?? "?" )
1674+ . Append ( "_" )
1675+ . Append ( diagnostic . Severity ? . ToString ( ) ?? "?" )
1676+ . Append ( "_" )
1677+ . Append ( start . Line )
1678+ . Append ( ":" )
1679+ . Append ( start . Character )
1680+ . Append ( "-" )
1681+ . Append ( end . Line )
1682+ . Append ( ":" )
1683+ . Append ( end . Character ) ;
1684+
1685+ var id = sb . ToString ( ) ;
1686+ return id ;
1687+ }
1688+
16421689 private static Diagnostic GetDiagnosticFromMarker ( ScriptFileMarker scriptFileMarker )
16431690 {
16441691 return new Diagnostic
16451692 {
16461693 Severity = MapDiagnosticSeverity ( scriptFileMarker . Level ) ,
16471694 Message = scriptFileMarker . Message ,
1648- Code = scriptFileMarker . Source + Guid . NewGuid ( ) . ToString ( ) ,
1695+ Code = scriptFileMarker . RuleName ,
16491696 Source = scriptFileMarker . Source ,
16501697 Range = new Range
16511698 {
0 commit comments