17
17
using System . Linq ;
18
18
using System . Management . Automation ;
19
19
using System . Management . Automation . Language ;
20
+ using System . Text ;
20
21
using System . Text . RegularExpressions ;
21
22
using System . Threading ;
22
23
using System . Threading . Tasks ;
@@ -27,7 +28,7 @@ namespace Microsoft.PowerShell.EditorServices.Protocol.Server
27
28
{
28
29
public class LanguageServer
29
30
{
30
- private static CancellationTokenSource existingRequestCancellation ;
31
+ private static CancellationTokenSource s_existingRequestCancellation ;
31
32
32
33
private static readonly Location [ ] s_emptyLocationResult = new Location [ 0 ] ;
33
34
@@ -48,6 +49,7 @@ public class LanguageServer
48
49
private LanguageServerEditorOperations editorOperations ;
49
50
private LanguageServerSettings currentSettings = new LanguageServerSettings ( ) ;
50
51
52
+ // The outer key is the file's uri, the inner key is a unique id for the diagnostic
51
53
private Dictionary < string , Dictionary < string , MarkerCorrection > > codeActionsPerFile =
52
54
new Dictionary < string , Dictionary < string , MarkerCorrection > > ( ) ;
53
55
@@ -1182,6 +1184,7 @@ private bool IsQueryMatch(string query, string symbolName)
1182
1184
return symbolName . IndexOf ( query , StringComparison . OrdinalIgnoreCase ) >= 0 ;
1183
1185
}
1184
1186
1187
+ // https://microsoft.github.io/language-server-protocol/specification#textDocument_codeAction
1185
1188
protected async Task HandleCodeActionRequest (
1186
1189
CodeActionParams codeActionParams ,
1187
1190
RequestContext < CodeActionCommand [ ] > requestContext )
@@ -1190,12 +1193,22 @@ protected async Task HandleCodeActionRequest(
1190
1193
Dictionary < string , MarkerCorrection > markerIndex = null ;
1191
1194
List < CodeActionCommand > codeActionCommands = new List < CodeActionCommand > ( ) ;
1192
1195
1196
+ // If there are any code fixes, send these commands first so they appear at top of "Code Fix" menu in the client UI.
1193
1197
if ( this . codeActionsPerFile . TryGetValue ( codeActionParams . TextDocument . Uri , out markerIndex ) )
1194
1198
{
1195
1199
foreach ( var diagnostic in codeActionParams . Context . Diagnostics )
1196
1200
{
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 ) )
1199
1212
{
1200
1213
codeActionCommands . Add (
1201
1214
new CodeActionCommand
@@ -1208,8 +1221,30 @@ protected async Task HandleCodeActionRequest(
1208
1221
}
1209
1222
}
1210
1223
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 ( ) ) ;
1213
1248
}
1214
1249
1215
1250
protected async Task HandleDocumentFormattingRequest (
@@ -1454,15 +1489,15 @@ private Task RunScriptDiagnostics(
1454
1489
// If there's an existing task, attempt to cancel it
1455
1490
try
1456
1491
{
1457
- if ( existingRequestCancellation != null )
1492
+ if ( s_existingRequestCancellation != null )
1458
1493
{
1459
1494
// Try to cancel the request
1460
- existingRequestCancellation . Cancel ( ) ;
1495
+ s_existingRequestCancellation . Cancel ( ) ;
1461
1496
1462
1497
// If cancellation didn't throw an exception,
1463
1498
// clean up the existing token
1464
- existingRequestCancellation . Dispose ( ) ;
1465
- existingRequestCancellation = null ;
1499
+ s_existingRequestCancellation . Dispose ( ) ;
1500
+ s_existingRequestCancellation = null ;
1466
1501
}
1467
1502
}
1468
1503
catch ( Exception e )
@@ -1479,11 +1514,17 @@ private Task RunScriptDiagnostics(
1479
1514
return cancelTask . Task ;
1480
1515
}
1481
1516
1517
+ // If filesToAnalzye is empty, nothing to do so return early.
1518
+ if ( filesToAnalyze . Length == 0 )
1519
+ {
1520
+ return Task . FromResult ( true ) ;
1521
+ }
1522
+
1482
1523
// Create a fresh cancellation token and then start the task.
1483
1524
// We create this on a different TaskScheduler so that we
1484
1525
// don't block the main message loop thread.
1485
1526
// TODO: Is there a better way to do this?
1486
- existingRequestCancellation = new CancellationTokenSource ( ) ;
1527
+ s_existingRequestCancellation = new CancellationTokenSource ( ) ;
1487
1528
Task . Factory . StartNew (
1488
1529
( ) =>
1489
1530
DelayThenInvokeDiagnostics (
@@ -1494,36 +1535,14 @@ private Task RunScriptDiagnostics(
1494
1535
editorSession ,
1495
1536
eventSender ,
1496
1537
this . Logger ,
1497
- existingRequestCancellation . Token ) ,
1538
+ s_existingRequestCancellation . Token ) ,
1498
1539
CancellationToken . None ,
1499
1540
TaskCreationOptions . None ,
1500
1541
TaskScheduler . Default ) ;
1501
1542
1502
1543
return Task . FromResult ( true ) ;
1503
1544
}
1504
1545
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
-
1527
1546
private static async Task DelayThenInvokeDiagnostics (
1528
1547
int delayMilliseconds ,
1529
1548
ScriptFile [ ] filesToAnalyze ,
@@ -1573,6 +1592,7 @@ private static async Task DelayThenInvokeDiagnostics(
1573
1592
1574
1593
await PublishScriptDiagnostics (
1575
1594
scriptFile ,
1595
+ // Concat script analysis errors to any existing parse errors
1576
1596
scriptFile . SyntaxMarkers . Concat ( semanticMarkers ) . ToArray ( ) ,
1577
1597
correctionIndex ,
1578
1598
eventSender ) ;
@@ -1620,7 +1640,8 @@ private static async Task PublishScriptDiagnostics(
1620
1640
Diagnostic markerDiagnostic = GetDiagnosticFromMarker ( marker ) ;
1621
1641
if ( marker . Correction != null )
1622
1642
{
1623
- fileCorrections . Add ( markerDiagnostic . Code , marker . Correction ) ;
1643
+ string diagnosticId = GetUniqueIdFromDiagnostic ( markerDiagnostic ) ;
1644
+ fileCorrections . Add ( diagnosticId , marker . Correction ) ;
1624
1645
}
1625
1646
1626
1647
diagnostics . Add ( markerDiagnostic ) ;
@@ -1639,13 +1660,39 @@ await eventSender(
1639
1660
} ) ;
1640
1661
}
1641
1662
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
+
1642
1689
private static Diagnostic GetDiagnosticFromMarker ( ScriptFileMarker scriptFileMarker )
1643
1690
{
1644
1691
return new Diagnostic
1645
1692
{
1646
1693
Severity = MapDiagnosticSeverity ( scriptFileMarker . Level ) ,
1647
1694
Message = scriptFileMarker . Message ,
1648
- Code = scriptFileMarker . Source + Guid . NewGuid ( ) . ToString ( ) ,
1695
+ Code = scriptFileMarker . RuleName ,
1649
1696
Source = scriptFileMarker . Source ,
1650
1697
Range = new Range
1651
1698
{
0 commit comments