diff --git a/internal/fourslash/fourslash.go b/internal/fourslash/fourslash.go index 2a3122a18bb..dc2bb148784 100644 --- a/internal/fourslash/fourslash.go +++ b/internal/fourslash/fourslash.go @@ -1913,9 +1913,13 @@ func (f *FourslashTest) VerifyCodeFixAll(t *testing.T, options VerifyCodeFixAllO // VerifySourceFixAll verifies that requesting a source.fixAll code action produces the expected file content. // This tests the on-save code path where VS Code requests source.fixAll. func (f *FourslashTest) VerifySourceFixAll(t *testing.T, expectedContent string) { + f.VerifySourceFixAllWithKind(t, expectedContent, lsproto.CodeActionKindSourceFixAll) +} + +func (f *FourslashTest) VerifySourceFixAllWithKind(t *testing.T, expectedContent string, codeActionKind lsproto.CodeActionKind) { t.Helper() - only := []lsproto.CodeActionKind{lsproto.CodeActionKindSourceFixAll} + only := []lsproto.CodeActionKind{codeActionKind} params := &lsproto.CodeActionParams{ TextDocument: lsproto.TextDocumentIdentifier{ Uri: lsconv.FileNameToDocumentURI(f.activeFilename), @@ -1937,7 +1941,7 @@ func (f *FourslashTest) VerifySourceFixAll(t *testing.T, expectedContent string) var selected *lsproto.CodeAction for _, item := range *result.CommandOrCodeActionArray { - if item.CodeAction == nil || item.CodeAction.Kind == nil || *item.CodeAction.Kind != lsproto.CodeActionKindSourceFixAll { + if item.CodeAction == nil || item.CodeAction.Kind == nil || *item.CodeAction.Kind != codeActionKind { continue } selected = item.CodeAction diff --git a/internal/fourslash/tests/organizeImports_coalesceImports_test.go b/internal/fourslash/tests/organizeImports_coalesceImports_test.go index 17ebba013f2..5f2a6de5405 100644 --- a/internal/fourslash/tests/organizeImports_coalesceImports_test.go +++ b/internal/fourslash/tests/organizeImports_coalesceImports_test.go @@ -25,6 +25,23 @@ M; n; B; y; O;`, ) } +func TestOrganizeImports_coalesceImportsTsKind(t *testing.T) { + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = `import x from "lib"; +import y from "lib"; +x; y;` + f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content) + defer done() + f.VerifyOrganizeImports( + t, + `import { default as x, default as y } from "lib"; +x; y;`, + lsproto.CodeActionKindSourceOrganizeImportsTs, + &lsutil.UserPreferences{OrganizeImportsSort: lsutil.OrganizeImportsSortOrdinalIgnoreCase}, + ) +} + func TestOrganizeImports_coalesceImports_combineSideEffectOnly(t *testing.T) { t.Parallel() defer testutil.RecoverAndFail(t, "Panic on fourslash test") diff --git a/internal/fourslash/tests/organizeImports_removeUnused_preservesMultiline_test.go b/internal/fourslash/tests/organizeImports_removeUnused_preservesMultiline_test.go index 6bc47bfe123..12aa4b787e6 100644 --- a/internal/fourslash/tests/organizeImports_removeUnused_preservesMultiline_test.go +++ b/internal/fourslash/tests/organizeImports_removeUnused_preservesMultiline_test.go @@ -60,6 +60,31 @@ export { a, c };`, ) } +func TestOrganizeImports_removeUnusedTsKind(t *testing.T) { + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = `import { + a, + b, + c, +} from "module"; + +export { a, c };` + f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content) + defer done() + f.VerifyOrganizeImports( + t, + `import { + a, + c +} from "module"; + +export { a, c };`, + lsproto.CodeActionKindSourceRemoveUnusedImportsTs, + nil, + ) +} + func TestOrganizeImports_removeUnusedUsesLanguageServiceFormatOptions(t *testing.T) { t.Parallel() defer testutil.RecoverAndFail(t, "Panic on fourslash test") diff --git a/internal/fourslash/tests/organizeImports_sortModuleSpecifiers_test.go b/internal/fourslash/tests/organizeImports_sortModuleSpecifiers_test.go index b3eff84f6d6..d2ae9d0ea5f 100644 --- a/internal/fourslash/tests/organizeImports_sortModuleSpecifiers_test.go +++ b/internal/fourslash/tests/organizeImports_sortModuleSpecifiers_test.go @@ -27,6 +27,24 @@ x; y;`, ) } +func TestOrganizeImports_sortModuleSpecifiersTsKind(t *testing.T) { + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = `import x from "lib2"; +import y from "lib1"; +x; y;` + f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content) + defer done() + f.VerifyOrganizeImports( + t, + `import y from "lib1"; +import x from "lib2"; +x; y;`, + lsproto.CodeActionKindSourceSortImportsTs, + &lsutil.UserPreferences{OrganizeImportsSort: lsutil.OrganizeImportsSortOrdinalIgnoreCase}, + ) +} + func TestOrganizeImports_sortModuleSpecifiers_relativeVsRelative(t *testing.T) { t.Parallel() defer testutil.RecoverAndFail(t, "Panic on fourslash test") diff --git a/internal/fourslash/tests/sourceFixAllImports_test.go b/internal/fourslash/tests/sourceFixAllImports_test.go index a4626b822e2..861153e9836 100644 --- a/internal/fourslash/tests/sourceFixAllImports_test.go +++ b/internal/fourslash/tests/sourceFixAllImports_test.go @@ -4,6 +4,7 @@ import ( "testing" "github.com/microsoft/typescript-go/internal/fourslash" + "github.com/microsoft/typescript-go/internal/lsp/lsproto" "github.com/microsoft/typescript-go/internal/testutil" ) @@ -53,3 +54,24 @@ import { b } from "./b"; a; b;`) } + +func TestSourceFixAllCodeActionTsKind(t *testing.T) { + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = `// @Filename: /a.ts +export const a: number = 1; +// @Filename: /b.ts +export const b: number = 2; +// @Filename: /main.ts +a; +b;` + f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content) + defer done() + f.GoToFile(t, "/main.ts") + + f.VerifySourceFixAllWithKind(t, `import { a } from "./a"; +import { b } from "./b"; + +a; +b;`, lsproto.CodeActionKindSourceFixAllTs) +} diff --git a/internal/ls/codeactions.go b/internal/ls/codeactions.go index d04f64c73d0..07f9566bad9 100644 --- a/internal/ls/codeactions.go +++ b/internal/ls/codeactions.go @@ -89,7 +89,7 @@ func (l *LanguageService) ProvideCodeActions(ctx context.Context, params *lsprot } if isFixAllKind(kind) { - fixAllAction, err := l.createFixAllAction(ctx, program, file, params.TextDocument.Uri) + fixAllAction, err := l.createFixAllAction(ctx, program, file, params.TextDocument.Uri, kind) if err != nil { return lsproto.CodeActionResponse{}, err } @@ -239,7 +239,7 @@ func codeActionKindContains(requestedKind, actionKind lsproto.CodeActionKind) bo // isFixAllKind returns true if the requested kind matches source.fixAll func isFixAllKind(kind lsproto.CodeActionKind) bool { - return codeActionKindContains(kind, lsproto.CodeActionKindSourceFixAll) + return codeActionKindContains(kind, lsproto.CodeActionKindSourceFixAllTs) } // wantsQuickFixes returns true if the Only filter is nil/empty (meaning all kinds are wanted) @@ -263,8 +263,12 @@ func (l *LanguageService) createFixAllAction( program *compiler.Program, file *ast.SourceFile, uri lsproto.DocumentUri, + requestedKind lsproto.CodeActionKind, ) (*lsproto.CommandOrCodeAction, error) { kind := lsproto.CodeActionKindSourceFixAll + if requestedKind == lsproto.CodeActionKindSourceFixAllTs { + kind = requestedKind + } lspChanges := make(map[lsproto.DocumentUri][]*lsproto.TextEdit) for _, provider := range codeFixProviders { @@ -303,7 +307,7 @@ func (l *LanguageService) createFixAllAction( // 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 { + switch getBaseOrganizeImportsKind(kind) { case lsproto.CodeActionKindSourceRemoveUnusedImports: return diagnostics.Remove_Unused_Imports.Localize(loc) case lsproto.CodeActionKindSourceSortImports: @@ -316,6 +320,13 @@ func getOrganizeImportsActionTitle(ctx context.Context, kind lsproto.CodeActionK // getOrganizeImportsActionsForKind returns the organize imports code action kinds that should be // returned for the given requested kind. func getOrganizeImportsActionsForKind(requestedKind lsproto.CodeActionKind) []lsproto.CodeActionKind { + switch requestedKind { + case lsproto.CodeActionKindSourceOrganizeImportsTs, + lsproto.CodeActionKindSourceRemoveUnusedImportsTs, + lsproto.CodeActionKindSourceSortImportsTs: + return []lsproto.CodeActionKind{requestedKind} + } + organizeImportsKinds := []lsproto.CodeActionKind{ lsproto.CodeActionKindSourceOrganizeImports, lsproto.CodeActionKindSourceRemoveUnusedImports, @@ -336,6 +347,19 @@ func getOrganizeImportsActionsForKind(requestedKind lsproto.CodeActionKind) []ls return result } +func getBaseOrganizeImportsKind(kind lsproto.CodeActionKind) lsproto.CodeActionKind { + switch kind { + case lsproto.CodeActionKindSourceOrganizeImportsTs: + return lsproto.CodeActionKindSourceOrganizeImports + case lsproto.CodeActionKindSourceRemoveUnusedImportsTs: + return lsproto.CodeActionKindSourceRemoveUnusedImports + case lsproto.CodeActionKindSourceSortImportsTs: + return lsproto.CodeActionKindSourceSortImports + default: + return kind + } +} + // createOrganizeImportsAction creates the organize imports code action func (l *LanguageService) createOrganizeImportsAction( ctx context.Context, diff --git a/internal/ls/codeactions_test.go b/internal/ls/codeactions_test.go new file mode 100644 index 00000000000..270feb12066 --- /dev/null +++ b/internal/ls/codeactions_test.go @@ -0,0 +1,33 @@ +package ls + +import ( + "testing" + + "github.com/microsoft/typescript-go/internal/lsp/lsproto" + "gotest.tools/v3/assert" +) + +func TestGetOrganizeImportsActionsForTypeScriptKinds(t *testing.T) { + t.Parallel() + + tests := []struct { + requested lsproto.CodeActionKind + expected lsproto.CodeActionKind + expectedBase lsproto.CodeActionKind + }{ + {lsproto.CodeActionKindSourceOrganizeImportsTs, lsproto.CodeActionKindSourceOrganizeImportsTs, lsproto.CodeActionKindSourceOrganizeImports}, + {lsproto.CodeActionKindSourceRemoveUnusedImportsTs, lsproto.CodeActionKindSourceRemoveUnusedImportsTs, lsproto.CodeActionKindSourceRemoveUnusedImports}, + {lsproto.CodeActionKindSourceSortImportsTs, lsproto.CodeActionKindSourceSortImportsTs, lsproto.CodeActionKindSourceSortImports}, + } + + for _, test := range tests { + assert.DeepEqual(t, getOrganizeImportsActionsForKind(test.requested), []lsproto.CodeActionKind{test.expected}) + assert.Equal(t, getBaseOrganizeImportsKind(test.requested), test.expectedBase) + } +} + +func TestIsFixAllKindAcceptsTypeScriptKind(t *testing.T) { + t.Parallel() + + assert.Assert(t, isFixAllKind(lsproto.CodeActionKindSourceFixAllTs)) +} diff --git a/internal/ls/organizeimports.go b/internal/ls/organizeimports.go index ddea88bffd1..b34befbf37d 100644 --- a/internal/ls/organizeimports.go +++ b/internal/ls/organizeimports.go @@ -28,6 +28,7 @@ func (l *LanguageService) OrganizeImports( kind lsproto.CodeActionKind, ) map[string][]*lsproto.TextEdit { changeTracker := change.NewTracker(ctx, program.Options(), l.FormatOptions(), l.converters) + kind = getBaseOrganizeImportsKind(kind) shouldSort := kind == lsproto.CodeActionKindSourceSortImports || kind == lsproto.CodeActionKindSourceOrganizeImports shouldCombine := shouldSort shouldRemove := kind == lsproto.CodeActionKindSourceRemoveUnusedImports || kind == lsproto.CodeActionKindSourceOrganizeImports diff --git a/internal/lsp/lsproto/lsp.go b/internal/lsp/lsproto/lsp.go index 152820346cc..331a69fd826 100644 --- a/internal/lsp/lsproto/lsp.go +++ b/internal/lsp/lsproto/lsp.go @@ -307,6 +307,10 @@ func PreferredMarkupKind(formats []MarkupKind) MarkupKind { } const ( - CodeActionKindSourceRemoveUnusedImports CodeActionKind = "source.removeUnusedImports" - CodeActionKindSourceSortImports CodeActionKind = "source.sortImports" + CodeActionKindSourceOrganizeImportsTs CodeActionKind = "source.organizeImports.ts" + CodeActionKindSourceRemoveUnusedImports CodeActionKind = "source.removeUnusedImports" + CodeActionKindSourceRemoveUnusedImportsTs CodeActionKind = "source.removeUnusedImports.ts" + CodeActionKindSourceSortImports CodeActionKind = "source.sortImports" + CodeActionKindSourceSortImportsTs CodeActionKind = "source.sortImports.ts" + CodeActionKindSourceFixAllTs CodeActionKind = "source.fixAll.ts" ) diff --git a/internal/lsp/server.go b/internal/lsp/server.go index 92566c0478d..2b978c6ffd6 100644 --- a/internal/lsp/server.go +++ b/internal/lsp/server.go @@ -1207,9 +1207,13 @@ func (s *Server) handleInitialize(ctx context.Context, params *lsproto.Initializ CodeActionKinds: &[]lsproto.CodeActionKind{ lsproto.CodeActionKindQuickFix, lsproto.CodeActionKindSourceOrganizeImports, + lsproto.CodeActionKindSourceOrganizeImportsTs, lsproto.CodeActionKindSourceRemoveUnusedImports, + lsproto.CodeActionKindSourceRemoveUnusedImportsTs, lsproto.CodeActionKindSourceSortImports, + lsproto.CodeActionKindSourceSortImportsTs, lsproto.CodeActionKindSourceFixAll, + lsproto.CodeActionKindSourceFixAllTs, }, }, }, diff --git a/internal/lsp/server_test.go b/internal/lsp/server_test.go index 6602f62fc01..396616ba59e 100644 --- a/internal/lsp/server_test.go +++ b/internal/lsp/server_test.go @@ -3,6 +3,7 @@ package lsp import ( "context" "io" + "slices" "testing" "time" @@ -11,6 +12,7 @@ import ( "github.com/microsoft/typescript-go/internal/lsp/lsproto" "github.com/microsoft/typescript-go/internal/project" "github.com/microsoft/typescript-go/internal/vfs/vfstest" + "gotest.tools/v3/assert" ) type shutdownTestReader struct{} @@ -21,6 +23,48 @@ type shutdownTestWriter struct{} func (shutdownTestWriter) Write(*lsproto.Message) error { return nil } +func TestInitializeAdvertisesTypeScriptSourceActionKinds(t *testing.T) { + t.Parallel() + + if !bundled.Embedded { + t.Skip("bundled files are not embedded") + } + + fs := bundled.WrapFS(vfstest.FromMap(map[string]string{}, false)) + server := NewServer(&ServerOptions{ + In: shutdownTestReader{}, + Out: shutdownTestWriter{}, + Err: io.Discard, + Cwd: "/home/projects", + FS: fs, + DefaultLibraryPath: bundled.LibPath(), + }) + server.backgroundCtx = t.Context() + + result, err := server.handleInitialize(t.Context(), &lsproto.InitializeParams{ + Capabilities: &lsproto.ClientCapabilities{}, + }, nil) + assert.NilError(t, err, "Initialize failed") + + codeActionProvider := result.Capabilities.CodeActionProvider + assert.Assert(t, codeActionProvider != nil && codeActionProvider.CodeActionOptions != nil) + kinds := codeActionProvider.CodeActionOptions.CodeActionKinds + assert.Assert(t, kinds != nil) + + for _, kind := range []lsproto.CodeActionKind{ + lsproto.CodeActionKindSourceOrganizeImports, + lsproto.CodeActionKindSourceOrganizeImportsTs, + lsproto.CodeActionKindSourceRemoveUnusedImports, + lsproto.CodeActionKindSourceRemoveUnusedImportsTs, + lsproto.CodeActionKindSourceSortImports, + lsproto.CodeActionKindSourceSortImportsTs, + lsproto.CodeActionKindSourceFixAll, + lsproto.CodeActionKindSourceFixAllTs, + } { + assert.Assert(t, slices.Contains(*kinds, kind), "missing code action kind %q", kind) + } +} + // TestServerShutdownNoDeadlock verifies that operations after shutdown // don't block. func TestServerShutdownNoDeadlock(t *testing.T) {