Skip to content

fix(lsp): support TypeScript source action kinds - #4496

Open
Andrew Ghostuhin (TorinAsakura) wants to merge 8 commits into
microsoft:mainfrom
TorinAsakura:fix/lsp-specific-source-action-kinds
Open

fix(lsp): support TypeScript source action kinds#4496
Andrew Ghostuhin (TorinAsakura) wants to merge 8 commits into
microsoft:mainfrom
TorinAsakura:fix/lsp-specific-source-action-kinds

Conversation

@TorinAsakura

Copy link
Copy Markdown
Contributor

Summary

  • Advertise TypeScript-specific source action kinds with the .ts suffix.
  • Keep the existing generic source action kinds for backward compatibility.
  • Accept TypeScript-specific only filters for organize imports and fix all code actions.
  • Return the requested TypeScript-specific kind when the client asks for it.

Fixes #3793.

Before

The server advertised only generic source action kinds:

quickfix
source.organizeImports
source.removeUnusedImports
source.sortImports
source.fixAll

A client could not target the TypeScript server specifically with a code actions on save entry such as source.removeUnusedImports.ts.

After

The server advertises both generic and TypeScript-specific source action kinds:

quickfix
source.organizeImports
source.organizeImports.ts
source.removeUnusedImports
source.removeUnusedImports.ts
source.sortImports
source.sortImports.ts
source.fixAll
source.fixAll.ts

Requests filtered to source.organizeImports.ts, source.removeUnusedImports.ts, source.sortImports.ts, or source.fixAll.ts now return matching TypeScript-specific code actions, while the existing generic kinds still work.

Validation

  • go test ./internal/fourslash/tests -run 'TestOrganizeImports_(sortModuleSpecifiersTsKind|coalesceImportsTsKind|removeUnusedTsKind)|TestSourceFixAllCodeActionTsKind'
  • go test ./internal/lsp ./internal/ls ./internal/fourslash/tests -run 'TestInitializeAdvertisesTypeScriptSourceActionKinds|TestGetOrganizeImportsActionsForTypeScriptKinds|TestIsFixAllKindAcceptsTypeScriptKind|TestSourceFixAllCodeAction|TestOrganizeImports_(sortModuleSpecifiersTsKind|coalesceImportsTsKind|removeUnused)'
  • go test ./internal/lsp ./internal/ls ./internal/fourslash/tests ./internal/lsp/lsproto

P.S. I did not find an existing microsoft/TypeScript precedent for these .ts source action kinds. This follows the LSP hierarchical kind model and keeps the generic kinds advertised and accepted for compatibility.

Copilot AI review requested due to automatic review settings June 30, 2026 21:27
@jakebailey

Copy link
Copy Markdown
Member

P.S. I did not find an existing microsoft/TypeScript precedent for these .ts source action kinds. This follows the LSP hierarchical kind model and keeps the generic kinds advertised and accepted for compatibility.

You should probably be looking at the vscode repo's typescript-language-features extension

That being said, if the old extensions could not do this, then is this critical to do?

@TorinAsakura

Copy link
Copy Markdown
Contributor Author

That being said, if the old extensions could not do this, then is this critical to do?

I checked vscode’s typescript-language-features too; same result, I don’t see any .ts-specific source action kinds there.

BTW, I re-read the issue, and I don’t think it’s rly about matching the current VS Code extension. It’s more about the multi-server case. With source.removeUnusedImports in codeActionsOnSave, the client is asking for a generic LSP source action, so any server attached to that doc can match it. source.removeUnusedImports.ts gives clients a TS-specific knob, while source.removeUnusedImports stays around for existing setups.

That’s why I wired the actual only handling too, not just the initialize list. Otherwise we’d advertise a kind that doesn’t do anything.

So yeah, it’s not something VS Code seems to need today. The PR is more about making that escape hatch available for clients that do need to split source actions between several servers.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated 1 comment.

Comment thread internal/lsp/server_capabilities_test.go Outdated
Comment thread internal/lsp/lsproto/lsp.go

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated no new comments.

@jakebailey

Copy link
Copy Markdown
Member

I did some digging; pyright/pylance do not do this for python, nor gopls for Go, nor rust-analyzer for Rust.

The only one that adds these suffixes is typescript-language-server, the old tsserver wrapper. That and eslint, biome, etc. So this is I guess some sort of JS ecosystem thing...

Comment thread internal/lsp/server_capabilities_test.go Outdated
@TorinAsakura

Copy link
Copy Markdown
Contributor Author

So this is I guess some sort of JS ecosystem thing...

Yeah, shit happens, especially in the JS ecosystem :)

So where do we land with this PR in the end? I still think .ts matches what the issue asks for, but I’d rather settle on one direction now than keep guessing.

Comment thread internal/ls/organizeimports.go Outdated
) map[string][]*lsproto.TextEdit {
changeTracker := change.NewTracker(ctx, program.Options(), l.FormatOptions(), l.converters)
shouldSort := kind == lsproto.CodeActionKindSourceSortImports || kind == lsproto.CodeActionKindSourceOrganizeImports
baseKind := getBaseOrganizeImportsKind(kind)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

arguably this could just be kind = getBaseOrganizeImportsKind(kind).

Or even, we have a method on the CodeActionKind type that strips the .ts suffix

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The first option makes sense, I’m changing that now. But I didn’t quite get the second part: I checked CodeActionKind on the current head and couldn’t find a method that strips the .ts suffix. Did you mean adding one, or am I missing an existing helper somewhere?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean that you could add one, yes, which would simplfiy things I think. But, maybe not that much...

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment thread internal/ls/codeactions.go Outdated
func isFixAllKind(kind lsproto.CodeActionKind) bool {
return codeActionKindContains(kind, lsproto.CodeActionKindSourceFixAll)
return codeActionKindContains(kind, lsproto.CodeActionKindSourceFixAll) ||
kind == lsproto.CodeActionKindSourceFixAllTs

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this even needed? Note the contains above. Does make me think that this could be simplified.

@TorinAsakura Andrew Ghostuhin (TorinAsakura) Aug 19, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something like this?

60fbce9

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, that feels weird. Maybe because the args are flipped?

@TorinAsakura Andrew Ghostuhin (TorinAsakura) Aug 19, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, maybe. You added that helper in #3382, so I took the order as intentional: requestedKind contains actionKind.

That’s why I used it like that. The check itself behaves correctly, but I see why it reads oddly here, since the returned kind can still be the generic one.

Do you mean we should flip the helper to take (actionKind, requestedKind) and update all call sites? I don’t want to flip this one call and accidentally stop source and source.fixAll from matching.

@jakebailey

Copy link
Copy Markdown
Member

So, overall, I think this PR's idea is fine, but I'm still unsure about the implementation; I feel like there must be a more simple form which just strips the .ts prefix off of incoming stuff, we expose the .ts names, and then the whole PR is a lot smaller.

Since we're about to move repos (like, within hours), I'm not sure there's time to redo it all. I think you can reopen it on the main TS repo or I can have a go. Perhaps I'm wrong!

@jakebailey Jake Bailey (jakebailey) added the Unmigrated PR This PR was open at the time of the repo move back to TypeScript label Aug 19, 2026
@TorinAsakura

Copy link
Copy Markdown
Contributor Author

I think we need to settle the shape here before rewriting this again.

We already normalize the TypeScript-specific kind before the organize-imports implementation runs. The part we cannot just drop is keeping the originally requested kind at the boundary: if the client asks for source.removeUnusedImports.ts and we return source.removeUnusedImports, that action is outside the requested only filter and the client can ignore it. The same applies to source.fixAll.ts.

So stripping .ts off the incoming kind still has to do four things:

  • advertise the .ts kinds;
  • recognize the exact .ts requests;
  • run the generic implementation;
  • return the requested .ts kind.

That is what this PR does. A generic TrimSuffix helper might save a few lines, but it does not remove the routing, and unless we constrain it back to these exact kinds it also starts treating arbitrary *.ts actions as TypeScript actions.

If you have a concrete smaller flow in mind, spell it out and I’ll rework it. But there must be a simpler form is not enough to safely redo the PR again, especially with the repo move happening in a few hours.

Otherwise, let’s make a call: take the current implementation, or move the issue to the main TypeScript repo and restart it there with the intended shape agreed upfront.

@jakebailey

Copy link
Copy Markdown
Member

Are you an agent, or are you pasting agent output into the comments? ☹️

@TorinAsakura

Andrew Ghostuhin (TorinAsakura) commented Aug 19, 2026

Copy link
Copy Markdown
Contributor Author

Are you an agent, or are you pasting agent output into the comments? ☹️

Look, this is starting to annoy me. I’m just trying to help the community, but you can’t seem to decide what you want yourself, and you keep trying to pin something on me. Let’s just get this over with.

@TorinAsakura

Copy link
Copy Markdown
Contributor Author

Are you an agent, or are you pasting agent output into the comments? ☹️

Let me try to clear this up. I’m Russian. If I want to express a complicated thought, I use a translator, because I can’t be bothered to sit around and think “in English.” Some things I write by hand, like this, and some I put through Apple Translate. Let’s solve the problems instead of giving me shit about how I write. Deal?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Unmigrated PR This PR was open at the time of the repo move back to TypeScript

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[lsp] make source action kinds more specific

4 participants