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, }, } }