400 lines
12 KiB
Go
400 lines
12 KiB
Go
package ls
|
|
|
|
import (
|
|
"cmp"
|
|
"context"
|
|
"slices"
|
|
"strings"
|
|
|
|
"github.com/microsoft/typescript-go/internal/ast"
|
|
"github.com/microsoft/typescript-go/internal/collections"
|
|
"github.com/microsoft/typescript-go/internal/compiler"
|
|
"github.com/microsoft/typescript-go/internal/core"
|
|
"github.com/microsoft/typescript-go/internal/diagnostics"
|
|
"github.com/microsoft/typescript-go/internal/locale"
|
|
"github.com/microsoft/typescript-go/internal/ls/lsconv"
|
|
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
|
|
)
|
|
|
|
// CodeFixProvider represents a provider for a specific type of code fix
|
|
type CodeFixProvider struct {
|
|
ErrorCodes []int32
|
|
GetCodeActions func(ctx context.Context, fixContext *CodeFixContext) ([]*CodeAction, error)
|
|
FixIds []string
|
|
GetAllCodeActions func(ctx context.Context, fixContext *CodeFixContext) (*CombinedCodeActions, error)
|
|
}
|
|
|
|
// CodeFixContext contains the context needed to generate code fixes
|
|
type CodeFixContext struct {
|
|
SourceFile *ast.SourceFile
|
|
Span core.TextRange
|
|
ErrorCode int32
|
|
Program *compiler.Program
|
|
LS *LanguageService
|
|
Diagnostic *lsproto.Diagnostic
|
|
Params *lsproto.CodeActionParams
|
|
}
|
|
|
|
// CodeAction represents a single code action fix
|
|
type CodeAction struct {
|
|
Description string
|
|
Changes []*lsproto.TextEdit
|
|
FixID string
|
|
FixAllDescription string
|
|
}
|
|
|
|
// Compare defines a total ordering for CodeAction values, comparing description
|
|
// then text edits lexicographically. Used with slices.BinarySearchFunc.
|
|
func (a *CodeAction) Compare(b *CodeAction) int {
|
|
if c := strings.Compare(a.Description, b.Description); c != 0 {
|
|
return c
|
|
}
|
|
if c := cmp.Compare(len(a.Changes), len(b.Changes)); c != 0 {
|
|
return c
|
|
}
|
|
for i, edit := range a.Changes {
|
|
if c := edit.Compare(b.Changes[i]); c != 0 {
|
|
return c
|
|
}
|
|
}
|
|
return 0
|
|
}
|
|
|
|
// CombinedCodeActions represents combined code actions for fix-all scenarios
|
|
type CombinedCodeActions struct {
|
|
Description string
|
|
Changes []*lsproto.TextEdit
|
|
}
|
|
|
|
// codeFixProviders is the list of all registered code fix providers
|
|
var codeFixProviders = []*CodeFixProvider{
|
|
ImportFixProvider,
|
|
IsolatedDeclarationsFixProvider,
|
|
FixClassIncorrectlyImplementsInterfaceProvider,
|
|
// Add more code fix providers here as they are implemented
|
|
}
|
|
|
|
// ProvideCodeActions returns code actions for the given range and context
|
|
func (l *LanguageService) ProvideCodeActions(ctx context.Context, params *lsproto.CodeActionParams) (lsproto.CodeActionResponse, error) {
|
|
program, file := l.getProgramAndFile(params.TextDocument.Uri)
|
|
|
|
var actions []lsproto.CommandOrCodeAction
|
|
|
|
if params.Context != nil && params.Context.Only != nil {
|
|
for _, kind := range *params.Context.Only {
|
|
matchingKinds := getOrganizeImportsActionsForKind(kind)
|
|
for _, matchingKind := range matchingKinds {
|
|
organizeAction := l.createOrganizeImportsAction(ctx, program, file, matchingKind)
|
|
actions = append(actions, *organizeAction)
|
|
}
|
|
|
|
if isFixAllKind(kind) {
|
|
fixAllAction, err := l.createFixAllAction(ctx, program, file, params.TextDocument.Uri)
|
|
if err != nil {
|
|
return lsproto.CodeActionResponse{}, err
|
|
}
|
|
if fixAllAction != nil {
|
|
actions = append(actions, *fixAllAction)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if params.Context != nil && params.Context.Diagnostics != nil && wantsQuickFixes(params.Context.Only) {
|
|
fixIdSeen := make(map[string]*CodeFixProvider)
|
|
|
|
var seen []*CodeAction // sorted for binary search dedup, dedup across all diagnostics and providers so if multiple diags produce the same codefix, only one is returned
|
|
|
|
for _, diag := range params.Context.Diagnostics {
|
|
if diag.Code == nil || diag.Code.Integer == nil {
|
|
continue
|
|
}
|
|
|
|
errorCode := *diag.Code.Integer
|
|
|
|
for _, provider := range codeFixProviders {
|
|
if !containsErrorCode(provider.ErrorCodes, errorCode) {
|
|
continue
|
|
}
|
|
|
|
position := l.converters.LineAndCharacterToPosition(file, diag.Range.Start)
|
|
endPosition := l.converters.LineAndCharacterToPosition(file, diag.Range.End)
|
|
fixContext := &CodeFixContext{
|
|
SourceFile: file,
|
|
Span: core.NewTextRange(int(position), int(endPosition)),
|
|
ErrorCode: errorCode,
|
|
Program: program,
|
|
LS: l,
|
|
Diagnostic: diag,
|
|
Params: params,
|
|
}
|
|
|
|
providerActions, err := provider.GetCodeActions(ctx, fixContext)
|
|
if err != nil {
|
|
return lsproto.CodeActionResponse{}, err
|
|
}
|
|
for _, action := range providerActions {
|
|
i, found := slices.BinarySearchFunc(seen, action, (*CodeAction).Compare)
|
|
if found {
|
|
continue
|
|
}
|
|
seen = slices.Insert(seen, i, action)
|
|
actions = append(actions, convertToLSPCodeAction(action, diag, params.TextDocument.Uri))
|
|
if action.FixID != "" {
|
|
fixIdSeen[action.FixID] = provider
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fixAllActions, err := l.getFixAllQuickFixes(ctx, program, file, params.TextDocument.Uri, fixIdSeen)
|
|
if err != nil {
|
|
return lsproto.CodeActionResponse{}, err
|
|
}
|
|
actions = append(actions, fixAllActions...)
|
|
}
|
|
|
|
return lsproto.CommandOrCodeActionArrayOrNull{CommandOrCodeActionArray: &actions}, nil
|
|
}
|
|
|
|
// getFixAllQuickFixes returns per-provider "Fix all in file" quickfix entries for providers
|
|
// that matched at least 2 diagnostics in the full file.
|
|
func (l *LanguageService) getFixAllQuickFixes(
|
|
ctx context.Context,
|
|
program *compiler.Program,
|
|
file *ast.SourceFile,
|
|
uri lsproto.DocumentUri,
|
|
fixIdSeen map[string]*CodeFixProvider,
|
|
) ([]lsproto.CommandOrCodeAction, error) {
|
|
var actions []lsproto.CommandOrCodeAction
|
|
|
|
// Deduplicate providers; multiple fixIds may map to the same provider.
|
|
var seen collections.Set[*CodeFixProvider]
|
|
for _, provider := range fixIdSeen {
|
|
if seen.Has(provider) {
|
|
continue
|
|
}
|
|
seen.Add(provider)
|
|
|
|
if provider.GetAllCodeActions == nil {
|
|
continue
|
|
}
|
|
|
|
if !hasMultipleFixableDiagnostics(ctx, program, file, provider.ErrorCodes) {
|
|
continue
|
|
}
|
|
|
|
fixContext := &CodeFixContext{
|
|
SourceFile: file,
|
|
Program: program,
|
|
LS: l,
|
|
}
|
|
combined, err := provider.GetAllCodeActions(ctx, fixContext)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if combined != nil && len(combined.Changes) > 0 {
|
|
kind := lsproto.CodeActionKindQuickFix
|
|
changes := map[lsproto.DocumentUri][]*lsproto.TextEdit{
|
|
uri: combined.Changes,
|
|
}
|
|
actions = append(actions, lsproto.CommandOrCodeAction{
|
|
CodeAction: &lsproto.CodeAction{
|
|
Title: combined.Description,
|
|
Kind: &kind,
|
|
Edit: &lsproto.WorkspaceEdit{Changes: &changes},
|
|
},
|
|
})
|
|
}
|
|
}
|
|
|
|
return actions, nil
|
|
}
|
|
|
|
// hasMultipleFixableDiagnostics returns true if the file has at least 2 diagnostics
|
|
// matching the given error codes. Checks all diagnostic sources (semantic,
|
|
// syntactic, suggestion, declaration) to match ProvideDiagnostics.
|
|
func hasMultipleFixableDiagnostics(ctx context.Context, program *compiler.Program, file *ast.SourceFile, errorCodes []int32) bool {
|
|
allDiags := getAllDiagnostics(ctx, program, file)
|
|
count := 0
|
|
for _, d := range allDiags {
|
|
if containsErrorCode(errorCodes, d.Code()) {
|
|
count++
|
|
if count >= 2 {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// codeActionKindContains returns true if the requested kind equals or is a
|
|
// hierarchical parent of actionKind, using '.' as the separator. This matches
|
|
// the semantics of VS Code's HierarchicalKind.contains.
|
|
func codeActionKindContains(requestedKind, actionKind lsproto.CodeActionKind) bool {
|
|
return requestedKind == actionKind ||
|
|
requestedKind == "" ||
|
|
strings.HasPrefix(string(actionKind), string(requestedKind)+".")
|
|
}
|
|
|
|
// isFixAllKind returns true if the requested kind matches source.fixAll
|
|
func isFixAllKind(kind lsproto.CodeActionKind) bool {
|
|
return codeActionKindContains(kind, lsproto.CodeActionKindSourceFixAll)
|
|
}
|
|
|
|
// wantsQuickFixes returns true if the Only filter is nil/empty (meaning all kinds are wanted)
|
|
// or explicitly includes the quickfix kind.
|
|
func wantsQuickFixes(only *[]lsproto.CodeActionKind) bool {
|
|
if only == nil || len(*only) == 0 {
|
|
return true
|
|
}
|
|
for _, kind := range *only {
|
|
if codeActionKindContains(kind, lsproto.CodeActionKindQuickFix) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// createFixAllAction creates a source.fixAll code action that applies all auto-fixable
|
|
// code fixes across the file.
|
|
func (l *LanguageService) createFixAllAction(
|
|
ctx context.Context,
|
|
program *compiler.Program,
|
|
file *ast.SourceFile,
|
|
uri lsproto.DocumentUri,
|
|
) (*lsproto.CommandOrCodeAction, error) {
|
|
kind := lsproto.CodeActionKindSourceFixAll
|
|
lspChanges := make(map[lsproto.DocumentUri][]*lsproto.TextEdit)
|
|
|
|
for _, provider := range codeFixProviders {
|
|
if provider.GetAllCodeActions == nil {
|
|
continue
|
|
}
|
|
|
|
fixContext := &CodeFixContext{
|
|
SourceFile: file,
|
|
Program: program,
|
|
LS: l,
|
|
}
|
|
|
|
combined, err := provider.GetAllCodeActions(ctx, fixContext)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if combined != nil && len(combined.Changes) > 0 {
|
|
lspChanges[uri] = append(lspChanges[uri], combined.Changes...)
|
|
}
|
|
}
|
|
|
|
if len(lspChanges) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
return &lsproto.CommandOrCodeAction{
|
|
CodeAction: &lsproto.CodeAction{
|
|
Title: diagnostics.Fix_All.Localize(locale.FromContext(ctx)),
|
|
Kind: &kind,
|
|
Edit: &lsproto.WorkspaceEdit{Changes: &lspChanges},
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
// getOrganizeImportsActionTitle returns the appropriate title for the given organize imports kind
|
|
func getOrganizeImportsActionTitle(ctx context.Context, kind lsproto.CodeActionKind) string {
|
|
loc := locale.FromContext(ctx)
|
|
switch kind {
|
|
case lsproto.CodeActionKindSourceRemoveUnusedImports:
|
|
return diagnostics.Remove_Unused_Imports.Localize(loc)
|
|
case lsproto.CodeActionKindSourceSortImports:
|
|
return diagnostics.Sort_Imports.Localize(loc)
|
|
default:
|
|
return diagnostics.Organize_Imports.Localize(loc)
|
|
}
|
|
}
|
|
|
|
// getOrganizeImportsActionsForKind returns the organize imports code action kinds that should be
|
|
// returned for the given requested kind.
|
|
func getOrganizeImportsActionsForKind(requestedKind lsproto.CodeActionKind) []lsproto.CodeActionKind {
|
|
organizeImportsKinds := []lsproto.CodeActionKind{
|
|
lsproto.CodeActionKindSourceOrganizeImports,
|
|
lsproto.CodeActionKindSourceRemoveUnusedImports,
|
|
lsproto.CodeActionKindSourceSortImports,
|
|
}
|
|
|
|
var result []lsproto.CodeActionKind
|
|
for _, organizeKind := range organizeImportsKinds {
|
|
if codeActionKindContains(requestedKind, organizeKind) {
|
|
result = append(result, organizeKind)
|
|
}
|
|
}
|
|
|
|
if slices.Contains(result, requestedKind) {
|
|
return []lsproto.CodeActionKind{requestedKind}
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// createOrganizeImportsAction creates the organize imports code action
|
|
func (l *LanguageService) createOrganizeImportsAction(
|
|
ctx context.Context,
|
|
program *compiler.Program,
|
|
file *ast.SourceFile,
|
|
kind lsproto.CodeActionKind,
|
|
) *lsproto.CommandOrCodeAction {
|
|
title := getOrganizeImportsActionTitle(ctx, kind)
|
|
changes := l.OrganizeImports(
|
|
ctx,
|
|
file,
|
|
program,
|
|
kind,
|
|
)
|
|
if len(changes) == 0 {
|
|
return &lsproto.CommandOrCodeAction{
|
|
CodeAction: &lsproto.CodeAction{
|
|
Title: title,
|
|
Kind: &kind,
|
|
Edit: &lsproto.WorkspaceEdit{Changes: &map[lsproto.DocumentUri][]*lsproto.TextEdit{}},
|
|
},
|
|
}
|
|
}
|
|
|
|
lspChanges := make(map[lsproto.DocumentUri][]*lsproto.TextEdit)
|
|
for fileName, edits := range changes {
|
|
fileURI := lsconv.FileNameToDocumentURI(fileName)
|
|
lspChanges[fileURI] = edits
|
|
}
|
|
|
|
return &lsproto.CommandOrCodeAction{
|
|
CodeAction: &lsproto.CodeAction{
|
|
Title: title,
|
|
Kind: &kind,
|
|
Edit: &lsproto.WorkspaceEdit{Changes: &lspChanges},
|
|
},
|
|
}
|
|
}
|
|
|
|
// containsErrorCode checks if the error code is in the list
|
|
func containsErrorCode(codes []int32, code int32) bool {
|
|
return slices.Contains(codes, code)
|
|
}
|
|
|
|
// convertToLSPCodeAction converts an internal CodeAction to an LSP CodeAction
|
|
func convertToLSPCodeAction(action *CodeAction, diag *lsproto.Diagnostic, uri lsproto.DocumentUri) lsproto.CommandOrCodeAction {
|
|
kind := lsproto.CodeActionKindQuickFix
|
|
changes := map[lsproto.DocumentUri][]*lsproto.TextEdit{
|
|
uri: action.Changes,
|
|
}
|
|
diagnostics := []*lsproto.Diagnostic{diag}
|
|
|
|
return lsproto.CommandOrCodeAction{
|
|
CodeAction: &lsproto.CodeAction{
|
|
Title: action.Description,
|
|
Kind: &kind,
|
|
Edit: &lsproto.WorkspaceEdit{Changes: &changes},
|
|
Diagnostics: &diagnostics,
|
|
},
|
|
}
|
|
}
|