Skip to content
8 changes: 6 additions & 2 deletions internal/fourslash/fourslash.go
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -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
Expand Down
17 changes: 17 additions & 0 deletions internal/fourslash/tests/organizeImports_coalesceImports_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
22 changes: 22 additions & 0 deletions internal/fourslash/tests/sourceFixAllImports_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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)
}
30 changes: 27 additions & 3 deletions internal/ls/codeactions.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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)
Expand All @@ -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 {
Expand Down Expand Up @@ -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:
Expand All @@ -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,
Expand All @@ -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,
Expand Down
33 changes: 33 additions & 0 deletions internal/ls/codeactions_test.go
Original file line number Diff line number Diff line change
@@ -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))
}
1 change: 1 addition & 0 deletions internal/ls/organizeimports.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 6 additions & 2 deletions internal/lsp/lsproto/lsp.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Comment thread
jakebailey marked this conversation as resolved.
)
4 changes: 4 additions & 0 deletions internal/lsp/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
},
},
Expand Down
44 changes: 44 additions & 0 deletions internal/lsp/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package lsp
import (
"context"
"io"
"slices"
"testing"
"time"

Expand All @@ -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{}
Expand All @@ -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) {
Expand Down