@@ -30,14 +30,16 @@ func ptrTo[T any](v T) *T {
30
30
31
31
// LSP Server implementation
32
32
type LSPServer struct {
33
- conn * jsonrpc2.Conn
34
- rootURI string
35
- documents map [lsproto.DocumentUri ]string // URI -> content
33
+ conn * jsonrpc2.Conn
34
+ rootURI string
35
+ documents map [lsproto.DocumentUri ]string // URI -> content
36
+ diagnostics map [lsproto.DocumentUri ][]rule.RuleDiagnostic // URI -> diagnostics
36
37
}
37
38
38
39
func NewLSPServer () * LSPServer {
39
40
return & LSPServer {
40
- documents : make (map [lsproto.DocumentUri ]string ),
41
+ documents : make (map [lsproto.DocumentUri ]string ),
42
+ diagnostics : make (map [lsproto.DocumentUri ][]rule.RuleDiagnostic ),
41
43
}
42
44
}
43
45
@@ -69,6 +71,8 @@ func (s *LSPServer) Handle(ctx context.Context, conn *jsonrpc2.Conn, req *jsonrp
69
71
case "textDocument/diagnostic" :
70
72
s .handleDidSave (ctx , req )
71
73
return nil , nil
74
+ case "textDocument/codeAction" :
75
+ return s .handleCodeAction (ctx , req )
72
76
case "shutdown" :
73
77
return s .handleShutdown (ctx , req )
74
78
@@ -102,6 +106,9 @@ func (s *LSPServer) handleInitialize(ctx context.Context, req *jsonrpc2.Request)
102
106
TextDocumentSync : & lsproto.TextDocumentSyncOptionsOrKind {
103
107
Kind : ptrTo (lsproto .TextDocumentSyncKindFull ),
104
108
},
109
+ CodeActionProvider : & lsproto.BooleanOrCodeActionOptions {
110
+ Boolean : ptrTo (true ),
111
+ },
105
112
},
106
113
}
107
114
@@ -155,6 +162,61 @@ func (s *LSPServer) handleShutdown(ctx context.Context, req *jsonrpc2.Request) (
155
162
return nil , nil
156
163
}
157
164
165
+ func (s * LSPServer ) handleCodeAction (ctx context.Context , req * jsonrpc2.Request ) (interface {}, error ) {
166
+ var params lsproto.CodeActionParams
167
+ if err := json .Unmarshal (* req .Params , & params ); err != nil {
168
+ return nil , & jsonrpc2.Error {
169
+ Code : jsonrpc2 .CodeParseError ,
170
+ Message : "Failed to parse code action params" ,
171
+ }
172
+ }
173
+
174
+ uri := params .TextDocument .Uri
175
+
176
+ // Get stored diagnostics for this document
177
+ ruleDiagnostics , exists := s .diagnostics [uri ]
178
+ if ! exists {
179
+ return []* lsproto.CodeAction {}, nil
180
+ }
181
+
182
+ var codeActions []* lsproto.CodeAction
183
+
184
+ // Find diagnostics that overlap with the requested range
185
+ for _ , ruleDiag := range ruleDiagnostics {
186
+ // Check if diagnostic range overlaps with requested range
187
+ diagStartLine , diagStartChar := scanner .GetLineAndCharacterOfPosition (ruleDiag .SourceFile , ruleDiag .Range .Pos ())
188
+ diagEndLine , diagEndChar := scanner .GetLineAndCharacterOfPosition (ruleDiag .SourceFile , ruleDiag .Range .End ())
189
+
190
+ diagRange := lsproto.Range {
191
+ Start : lsproto.Position {Line : uint32 (diagStartLine ), Character : uint32 (diagStartChar )},
192
+ End : lsproto.Position {Line : uint32 (diagEndLine ), Character : uint32 (diagEndChar )},
193
+ }
194
+
195
+ if rangesOverlap (diagRange , params .Range ) {
196
+ // Add code action for fixes
197
+ codeAction := createCodeActionFromRuleDiagnostic (ruleDiag , uri )
198
+ if codeAction != nil {
199
+ codeActions = append (codeActions , codeAction )
200
+ }
201
+ // add extract disable rule actions
202
+ disableActions := createDisableRuleActions (ruleDiag , uri )
203
+ codeActions = append (codeActions , disableActions ... )
204
+
205
+ // Add code actions for suggestions
206
+ if ruleDiag .Suggestions != nil {
207
+ for _ , suggestion := range * ruleDiag .Suggestions {
208
+ suggestionAction := createCodeActionFromSuggestion (ruleDiag , suggestion , uri )
209
+ if suggestionAction != nil {
210
+ codeActions = append (codeActions , suggestionAction )
211
+ }
212
+ }
213
+ }
214
+ }
215
+ }
216
+
217
+ return codeActions , nil
218
+ }
219
+
158
220
func (s * LSPServer ) runDiagnostics (ctx context.Context , uri lsproto.DocumentUri , content string ) {
159
221
uriString := string (uri )
160
222
@@ -256,6 +318,9 @@ func (s *LSPServer) runDiagnostics(ctx context.Context, uri lsproto.DocumentUri,
256
318
log .Printf ("Error running lint: %v" , err )
257
319
}
258
320
321
+ // Store rule diagnostics for code actions
322
+ s .diagnostics [uri ] = rule_diags
323
+
259
324
for _ , diagnostic := range rule_diags {
260
325
lspDiag := convertRuleDiagnosticToLSP (diagnostic , content )
261
326
lsp_diagnostics = append (lsp_diagnostics , lspDiag )
@@ -382,3 +447,215 @@ func runLintWithPrograms(uri lsproto.DocumentUri, programs []*compiler.Program,
382
447
}
383
448
return diagnostics , nil
384
449
}
450
+
451
+ // Helper function to check if two ranges overlap
452
+ func rangesOverlap (a , b lsproto.Range ) bool {
453
+ return ! (a .End .Line < b .Start .Line ||
454
+ (a .End .Line == b .Start .Line && a .End .Character < b .Start .Character ) ||
455
+ b .End .Line < a .Start .Line ||
456
+ (b .End .Line == a .Start .Line && b .End .Character < a .Start .Character ))
457
+ }
458
+
459
+ // Helper function to create a code action from a rule diagnostic
460
+ func createCodeActionFromRuleDiagnostic (ruleDiag rule.RuleDiagnostic , uri lsproto.DocumentUri ) * lsproto.CodeAction {
461
+ fixes := ruleDiag .Fixes ()
462
+ if len (fixes ) == 0 {
463
+ return nil
464
+ }
465
+
466
+ // Convert rule fixes to LSP text edits
467
+ var textEdits []* lsproto.TextEdit
468
+ for _ , fix := range fixes {
469
+ startLine , startChar := scanner .GetLineAndCharacterOfPosition (ruleDiag .SourceFile , fix .Range .Pos ())
470
+ endLine , endChar := scanner .GetLineAndCharacterOfPosition (ruleDiag .SourceFile , fix .Range .End ())
471
+
472
+ textEdit := & lsproto.TextEdit {
473
+ Range : lsproto.Range {
474
+ Start : lsproto.Position {Line : uint32 (startLine ), Character : uint32 (startChar )},
475
+ End : lsproto.Position {Line : uint32 (endLine ), Character : uint32 (endChar )},
476
+ },
477
+ NewText : fix .Text ,
478
+ }
479
+ textEdits = append (textEdits , textEdit )
480
+ }
481
+
482
+ // Create workspace edit
483
+ workspaceEdit := & lsproto.WorkspaceEdit {
484
+ Changes : & map [lsproto.DocumentUri ][]* lsproto.TextEdit {
485
+ uri : textEdits ,
486
+ },
487
+ }
488
+
489
+ // Create the corresponding LSP diagnostic for reference
490
+ diagStartLine , diagStartChar := scanner .GetLineAndCharacterOfPosition (ruleDiag .SourceFile , ruleDiag .Range .Pos ())
491
+ diagEndLine , diagEndChar := scanner .GetLineAndCharacterOfPosition (ruleDiag .SourceFile , ruleDiag .Range .End ())
492
+
493
+ lspDiagnostic := & lsproto.Diagnostic {
494
+ Range : lsproto.Range {
495
+ Start : lsproto.Position {Line : uint32 (diagStartLine ), Character : uint32 (diagStartChar )},
496
+ End : lsproto.Position {Line : uint32 (diagEndLine ), Character : uint32 (diagEndChar )},
497
+ },
498
+ Severity : ptrTo (lsproto .DiagnosticSeverity (ruleDiag .Severity .Int ())),
499
+ Source : ptrTo ("rslint" ),
500
+ Message : fmt .Sprintf ("[%s] %s" , ruleDiag .RuleName , ruleDiag .Message .Description ),
501
+ }
502
+
503
+ return & lsproto.CodeAction {
504
+ Title : fmt .Sprintf ("Fix: %s" , ruleDiag .Message .Description ),
505
+ Kind : ptrTo (lsproto .CodeActionKind ("quickfix" )),
506
+ Edit : workspaceEdit ,
507
+ Diagnostics : & []* lsproto.Diagnostic {lspDiagnostic },
508
+ IsPreferred : ptrTo (true ), // Mark auto-fixes as preferred
509
+ }
510
+ }
511
+
512
+ // Helper function to create a code action from a rule suggestion
513
+ func createCodeActionFromSuggestion (ruleDiag rule.RuleDiagnostic , suggestion rule.RuleSuggestion , uri lsproto.DocumentUri ) * lsproto.CodeAction {
514
+ fixes := suggestion .Fixes ()
515
+ if len (fixes ) == 0 {
516
+ return nil
517
+ }
518
+
519
+ // Convert rule fixes to LSP text edits
520
+ var textEdits []* lsproto.TextEdit
521
+ for _ , fix := range fixes {
522
+ startLine , startChar := scanner .GetLineAndCharacterOfPosition (ruleDiag .SourceFile , fix .Range .Pos ())
523
+ endLine , endChar := scanner .GetLineAndCharacterOfPosition (ruleDiag .SourceFile , fix .Range .End ())
524
+
525
+ textEdit := & lsproto.TextEdit {
526
+ Range : lsproto.Range {
527
+ Start : lsproto.Position {Line : uint32 (startLine ), Character : uint32 (startChar )},
528
+ End : lsproto.Position {Line : uint32 (endLine ), Character : uint32 (endChar )},
529
+ },
530
+ NewText : fix .Text ,
531
+ }
532
+ textEdits = append (textEdits , textEdit )
533
+ }
534
+
535
+ // Create workspace edit
536
+ workspaceEdit := & lsproto.WorkspaceEdit {
537
+ Changes : & map [lsproto.DocumentUri ][]* lsproto.TextEdit {
538
+ uri : textEdits ,
539
+ },
540
+ }
541
+
542
+ // Create the corresponding LSP diagnostic for reference
543
+ diagStartLine , diagStartChar := scanner .GetLineAndCharacterOfPosition (ruleDiag .SourceFile , ruleDiag .Range .Pos ())
544
+ diagEndLine , diagEndChar := scanner .GetLineAndCharacterOfPosition (ruleDiag .SourceFile , ruleDiag .Range .End ())
545
+
546
+ lspDiagnostic := & lsproto.Diagnostic {
547
+ Range : lsproto.Range {
548
+ Start : lsproto.Position {Line : uint32 (diagStartLine ), Character : uint32 (diagStartChar )},
549
+ End : lsproto.Position {Line : uint32 (diagEndLine ), Character : uint32 (diagEndChar )},
550
+ },
551
+ Severity : ptrTo (lsproto .DiagnosticSeverity (ruleDiag .Severity .Int ())),
552
+ Source : ptrTo ("rslint" ),
553
+ Message : fmt .Sprintf ("[%s] %s" , ruleDiag .RuleName , ruleDiag .Message .Description ),
554
+ }
555
+
556
+ return & lsproto.CodeAction {
557
+ Title : fmt .Sprintf ("Suggestion: %s" , suggestion .Message .Description ),
558
+ Kind : ptrTo (lsproto .CodeActionKind ("quickfix" )),
559
+ Edit : workspaceEdit ,
560
+ Diagnostics : & []* lsproto.Diagnostic {lspDiagnostic },
561
+ IsPreferred : ptrTo (false ), // Mark suggestions as not preferred
562
+ }
563
+ }
564
+
565
+ // Helper function to create disable rule actions for diagnostics without fixes
566
+ func createDisableRuleActions (ruleDiag rule.RuleDiagnostic , uri lsproto.DocumentUri ) []* lsproto.CodeAction {
567
+ var actions []* lsproto.CodeAction
568
+
569
+ // Create the corresponding LSP diagnostic for reference
570
+ diagStartLine , diagStartChar := scanner .GetLineAndCharacterOfPosition (ruleDiag .SourceFile , ruleDiag .Range .Pos ())
571
+ diagEndLine , diagEndChar := scanner .GetLineAndCharacterOfPosition (ruleDiag .SourceFile , ruleDiag .Range .End ())
572
+
573
+ lspDiagnostic := & lsproto.Diagnostic {
574
+ Range : lsproto.Range {
575
+ Start : lsproto.Position {Line : uint32 (diagStartLine ), Character : uint32 (diagStartChar )},
576
+ End : lsproto.Position {Line : uint32 (diagEndLine ), Character : uint32 (diagEndChar )},
577
+ },
578
+ Severity : ptrTo (lsproto .DiagnosticSeverity (ruleDiag .Severity .Int ())),
579
+ Source : ptrTo ("rslint" ),
580
+ Message : fmt .Sprintf ("[%s] %s" , ruleDiag .RuleName , ruleDiag .Message .Description ),
581
+ }
582
+
583
+ // Action 1: Disable rule for this line
584
+ disableLineAction := createDisableRuleForLineAction (ruleDiag , uri , lspDiagnostic )
585
+ if disableLineAction != nil {
586
+ actions = append (actions , disableLineAction )
587
+ }
588
+
589
+ // Action 2: Disable rule for entire file
590
+ disableFileAction := createDisableRuleForFileAction (ruleDiag , uri , lspDiagnostic )
591
+ if disableFileAction != nil {
592
+ actions = append (actions , disableFileAction )
593
+ }
594
+
595
+ return actions
596
+ }
597
+
598
+ // Helper function to create a "disable rule for this line" action
599
+ func createDisableRuleForLineAction (ruleDiag rule.RuleDiagnostic , uri lsproto.DocumentUri , lspDiagnostic * lsproto.Diagnostic ) * lsproto.CodeAction {
600
+ // Get the line where the diagnostic occurs
601
+ lineStart := lspDiagnostic .Range .Start .Line
602
+
603
+ // Create text edit to add eslint-disable-next-line comment
604
+ disableComment := fmt .Sprintf ("// eslint-disable-next-line %s\n " , ruleDiag .RuleName )
605
+
606
+ // Find the start of the line to insert the comment
607
+ lineStartPos := lsproto.Position {Line : lineStart , Character : 0 }
608
+
609
+ textEdit := & lsproto.TextEdit {
610
+ Range : lsproto.Range {
611
+ Start : lineStartPos ,
612
+ End : lineStartPos ,
613
+ },
614
+ NewText : disableComment ,
615
+ }
616
+
617
+ workspaceEdit := & lsproto.WorkspaceEdit {
618
+ Changes : & map [lsproto.DocumentUri ][]* lsproto.TextEdit {
619
+ uri : {textEdit },
620
+ },
621
+ }
622
+
623
+ return & lsproto.CodeAction {
624
+ Title : fmt .Sprintf ("Disable %s for this line" , ruleDiag .RuleName ),
625
+ Kind : ptrTo (lsproto .CodeActionKind ("quickfix" )),
626
+ Edit : workspaceEdit ,
627
+ Diagnostics : & []* lsproto.Diagnostic {lspDiagnostic },
628
+ IsPreferred : ptrTo (false ),
629
+ }
630
+ }
631
+
632
+ // Helper function to create a "disable rule for entire file" action
633
+ func createDisableRuleForFileAction (ruleDiag rule.RuleDiagnostic , uri lsproto.DocumentUri , lspDiagnostic * lsproto.Diagnostic ) * lsproto.CodeAction {
634
+ // Create text edit to add eslint-disable comment at the top of the file
635
+ disableComment := fmt .Sprintf ("/* eslint-disable %s */\n " , ruleDiag .RuleName )
636
+
637
+ // Insert at the very beginning of the file
638
+ fileStartPos := lsproto.Position {Line : 0 , Character : 0 }
639
+
640
+ textEdit := & lsproto.TextEdit {
641
+ Range : lsproto.Range {
642
+ Start : fileStartPos ,
643
+ End : fileStartPos ,
644
+ },
645
+ NewText : disableComment ,
646
+ }
647
+
648
+ workspaceEdit := & lsproto.WorkspaceEdit {
649
+ Changes : & map [lsproto.DocumentUri ][]* lsproto.TextEdit {
650
+ uri : {textEdit },
651
+ },
652
+ }
653
+
654
+ return & lsproto.CodeAction {
655
+ Title : fmt .Sprintf ("Disable %s for entire file" , ruleDiag .RuleName ),
656
+ Kind : ptrTo (lsproto .CodeActionKind ("quickfix" )),
657
+ Edit : workspaceEdit ,
658
+ Diagnostics : & []* lsproto.Diagnostic {lspDiagnostic },
659
+ IsPreferred : ptrTo (false ),
660
+ }
661
+ }
0 commit comments