Skip to content

Change InteractiveClient to instead by an LSP client feature - #6078

Merged
DanTup merged 7 commits into
interactive-formsfrom
interactive-forms-as-lsl-client-feature
Jun 16, 2026
Merged

DanTup merged 7 commits into
interactive-formsfrom
interactive-forms-as-lsl-client-feature

Conversation

@DanTup

@DanTup DanTup commented Jun 8, 2026

Copy link
Copy Markdown
Member

This makes it easier to conditionally register, and doesn't interfere if a client already has its own hierarchy.

@DanTup DanTup added this to the v3.138.0 milestone Jun 8, 2026
@DanTup

DanTup commented Jun 8, 2026

Copy link
Copy Markdown
Member Author

@h9jiang what are your thoughts on this?

Note: I don't have automated tests for this yet, but I'd like to add some so that changes like this don't rely on manual testing.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request refactors the interactive forms handling by replacing the InteractiveLanguageClient subclass with a standalone InteractiveFormsFeature class that wraps a standard LanguageClient. The review comments identify a critical TypeScript compilation issue where the protected method handleFailedRequest is accessed directly on the LanguageClient instance, and suggest casting the client to any to resolve the error.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread src/extension/analysis/form.ts Outdated
Comment thread src/extension/analysis/form.ts
Comment thread src/extension/analysis/form.ts
@codecov

codecov Bot commented Jun 8, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 12.50000% with 21 lines in your changes missing coverage. Please review.
✅ Project coverage is 70.03%. Comparing base (bef1492) to head (5267e57).
⚠️ Report is 20 commits behind head on interactive-forms.

Files with missing lines Patch % Lines
src/extension/analysis/form.ts 5.00% 19 Missing ⚠️
src/extension/analysis/analyzer.ts 33.33% 2 Missing ⚠️
Additional details and impacted files
@@                  Coverage Diff                  @@
##           interactive-forms    #6078      +/-   ##
=====================================================
- Coverage              70.11%   70.03%   -0.09%     
=====================================================
  Files                    173      173              
  Lines                  13234    13265      +31     
  Branches                2661     2665       +4     
=====================================================
+ Hits                    9279     9290      +11     
- Misses                  3456     3478      +22     
+ Partials                 499      497       -2     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@DanTup DanTup self-assigned this Jun 10, 2026
@DanTup
DanTup force-pushed the interactive-forms-as-lsl-client-feature branch from 48d42d5 to 18d3efb Compare June 10, 2026 16:55
@h9jiang

h9jiang commented Jun 10, 2026 •

Copy link
Copy Markdown
Collaborator

Hi Danny,

I tried to pull the changes from form.ts from dart-code back to vscode-go, other than the issue mentioned previously "the method name change from dispose() -> clear()", I do not observe any compatibility issue. (I wish vscode api have compatibility promise...). This also means, it is time for me to bump the vscode api version. :D

I will take a deeper look later this week, and left my comments before the next Monday. If you have any deadline for this, could you let me know so I prioritize reviewing this before others.

I'm still busy with the extension release and go release.

Thank you so much for putting this together!

@DanTup

DanTup commented Jun 10, 2026

Copy link
Copy Markdown
Member Author

This also means, it is time for me to bump the vscode api version. :D

I think the difference is in the VS Code LSP client, not VS Code API itself?

If the changes are only in the pre-release version, you could stick with the current stable one (and renamed the methods), and Dart-Code can be the "forked" version for now until it ships. Once things have settled down a bit, perhaps we should also create a repo to store a source-of-truth version of this code (and perhaps add some automated tests)?

If you have any deadline for this, could you let me know so I prioritize reviewing this before others.

No real deadline. It would be nice if it was merged in before the next release (around the end of the month) to make it easier for others to test, but it will still be behind a flag for that release at least. If you don't get chance to review things, I'm also not against merging code in we want to ship, and then addressing any feedback later (again, we can consider this a "forked" version in the short-term, and then align on the exact code after).

Thanks!

Comment thread src/extension/analysis/form.ts Outdated
Comment thread src/extension/analysis/form.ts Outdated
const middleware = interactiveOptions.middleware;
export class InteractiveFormsFeature {
constructor(private readonly client: LanguageClient) {
this.addMiddleware();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This may seems to be a naive question, but maybe there is further optimization we can do here. I'm still new to the static feature registration mechanism, please correct me if I'm wrong.

Is it possible to put this function call this.addMiddleware() to initialize() method? IIUC, the initialize() is called when the language server returns the server capabilities.

So in initialize(), we could already check if the server support interactive refactoring, then we can call addMiddleware conditionally based on whether the language server have the support for interactivity.

But this can only be done if the language client promises to wait for all the feature's (static / dynamic) initialize() to finish before executing any LSP methods. (Basically add the middleware after the initialize request is done but before any other LSP request is sent)

Right now, I think, the implementation is always add the middleware and we check the "server capability" in every execute command request.

Let me know what you think.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Good question! It turns out we can't do this in initialize because the built-in ExecuteCommandFeature captures the middleware inside initialize() (via register()) and therefore we install it too late.

However, there is a preInitialize() that fires for all features first, and it does work there. It does rely on ExecuteCommandFeature never switching to capture middleware during preInitialize though.

I've pushed this change - please take a look. I didn't duplicate checking the kinds of support for installing middleware though, only that the object exists at all. Seem reasonable?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Hi Danny, I think after taking another look, I think it is not the ideal. Sorry for giving a bad suggestion.

It turns out there two kinds of restart,

  • restart by creating a new language client and a new language server process
  • restart by only create new language server process (can be done by errorHandler return CloseAction.Restart)

Consider the addMiddleware function a "swap" function that replace the existing execute command middleware with our version... It works fine in first scenario but not the second one.

In the first scenario, the langauge client will be disposed, so it is a fresh client and the middleware.executeCommand is the user provided one. However, in the second case, the client is not disposed, so the middleware.executeCommand is already swapped by us. Then we "swap" again. To fix this,

  • use the original approach, manually check server capability on executeCommand
  • have some memorized boolean, indicating whether the executeCommand has been swapped. If is already swapped, stop swap it. If the server originally support it, now no longer, we have to "parse" it again.

So my suggestion would be revert to the original state. I'm sincerely sorry for the confusion caused by me.

Comment thread src/extension/analysis/analyzer.ts Outdated
export class LspAnalyzer extends Analyzer {
public readonly client: LanguageClient;
public readonly fileTracker: FileTracker;
private readonly interactiveClient: InteractiveFormsFeature;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Just a curious question, is making the feature private property necessary or it is because following the practices.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Generally I prefer to keep things private if not needed (to avoid accidentally depending on something that wasn't intended). The others are public only because they are exposed as part of the private API used by test runs.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Oh, maybe I misunderstood the question. If the question was "why is it private when others are public", the comment above is correct.

If the question is why do we have the field at all, then the answer is, I don't know - I did previously start writing tests (but aborted because I wanted to lock down the API first), so maybe I was exposing it for that. So, I have removed it.

And a related question, is "why do we have a .feature" field in InteractiveFormsFeature and I also think there's no good reason (I copied this pattern from a previous feature, where I had adapted a previous Provider into a feature). So I've also removed that.

So now the registration code is simply:

this.client.registerFeature(new InteractiveFormsFeature(this.client));

No field in the analyzer class, and no .feature on the end.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Sounds good, if we need to write test for it, I'm also agree we can make it public for convenience.

@h9jiang h9jiang left a comment •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Hi Danny, I think the idea of migrating from subclass to static feature make sense. This avoid the inherit problem as you mentioned. I left a few comments in the PR. Please take a look.

Over all the PR looks good to me. The only important comment is whether we can make the addMiddleware to initialize() method.

But just to confirm a developer user journal will looks like below:

  • Copy the form.ts
  • Write the middleware like below
export class MyExtensionAnalyzer {
	public readonly client: LanguageClient;
	private readonly interactiveClient: InteractiveFormsFeature;
	constructor(serverOptions: ServerOptions) {
		// 2. Type your middleware as InteractiveMiddleware
		const middleware: InteractiveMiddleware = {
			provideHover: (document, position, token, next) => next(document, position, token),
		};
		const clientOptions: InteractiveLanguageClientOptions = {
			documentSelector: [{ scheme: 'file', language: 'mydsl' }],
			middleware,
		};
		this.client = new LanguageClient("myDSLClient", "My DSL Server", serverOptions, clientOptions);
		// 3. Set up and register the interactive feature
		this.interactiveClient = new InteractiveFormsFeature(this.client);
		this.client.registerFeature(this.interactiveClient.feature);
		void this.client.start();
	}
}

My understanding is, the extension author can either create the client using middleware or the InteractiveMiddleware, either one of them is accepted. By creating a regular middleware means the extension author does not need any interactive refactoring customization. (just calling next()). If the extension author create a InteractiveMiddleware, the extension author can customize lsp (relevant to the interactive refactoring). Am I understanding the user journey correctly?

@h9jiang

h9jiang commented Jun 14, 2026

Copy link
Copy Markdown
Collaborator

I think the difference is in the VS Code LSP client, not VS Code API itself?

Yes, you are correct. Let me bump the LSP client version in vscode-go.

If the changes are only in the pre-release version, you could stick with the current stable one (and renamed the methods), and Dart-Code can be the "forked" version for now until it ships.

For adoption and releasing, this is what I'm thinking. For vscode-go, we already released v0.54.0, using the "sublass" solution. At the end of the day, no one can import "form.ts" directly from vscode-go and we have never announced this publicly ever. So consider the current implementation using "subclass" solution a prototype implementation for the proposal that can be deprecated anytime.

I will update vscode-go to use "static feature" solution once your CL is merged. When we actually announce it, we will have another vscode-go release (e.g. v0.54.1 or v0.56.0) that is using "static feature" solution.

So when we announce this, we can make sure both the dart-code and vscode-go extension use the same "static feature" solution.

Once things have settled down a bit, perhaps we should also create a repo to store a source-of-truth version of this code (and perhaps add some automated tests)?

I think this would make the most sense to have it prepared somewhere serving like a library. (but we will need to maintain it if we ever publish it as a package / module). Companies does have a little bit restrictive rule regarding in term of "releasing" especially it is released under an "organization". So I'm also ok with just asking the extension author to copy the form.ts around since it is pretty compatible. We could discuss offline with Brian and see if he has any suggestion.

@DanTup

DanTup commented Jun 15, 2026 •

Copy link
Copy Markdown
Member Author

link But just to confirm a developer user journal will looks like below: [...] Am I understanding the user journey correctly?

Yep, that's correct. Although I made some tweaks to simplify the feature registration a little (removing .feature and the field), see my reply to your comment above.

Yes, you are correct. Let me bump the LSP client version in vscode-go.

I don't recall if the required changes are in a stable version or only the last pre-release. If only the pre-release and you want to stick with the stable version for now, we could make a central version against that, and just have a slightly-tweaked version in Dart-Code until the LSP client library ships stable. I don't know what your appetite is for using a pre-release LSP client.

So when we announce this, we can make sure both the dart-code and vscode-go extension use the same "static feature" solution.

That sounds fine, although I don't think it has to be a requirement that we're on exactly the same code immediately, since as you note it's just internal code nobody else is referencing. If we need to iterate on this a bit more, it needn't hold up your announcement of the feature?

So I'm also ok with just asking the extension author to copy the form.ts around since it is pretty compatible. We could discuss offline with Brian and see if he has any suggestion.

Yeah, I think that's also fine (particularly while we're still tweaking things and shipping it ourselves). Although, it might still be good to have a single central place for form.ts, with some automated tests. If there are any complications in hosting that in a Go (or Dart - cc @bwilkerson) repo, we could also easily have a repo here in the Dart-Code organization (even if just temporarily).

@bwilkerson

Copy link
Copy Markdown

There shouldn't be any complications related to hosting it in the dart-lang organization, but I'm assuming that eventually we'll want to have a language-agnostic repo that all the shared code lives in, so that other language servers can depend on it without the encumbrance of depending on either dart or go.

@h9jiang

h9jiang commented Jun 15, 2026 •

Copy link
Copy Markdown
Collaborator

Hi @DanTup, thanks for preparing the CL, I think the CL looks good to me. The only comment I have is regarding the preInitialize function which is caused by my bad suggestion. Please take a look.

Other than that, this looks good to me. (I don't think I can cast a vote though), consider it +2 other than the one comment above.

(I can sync vscode-go once you merge this PR or if you want to become a contributor to vscode-go :D)

For publishing code, I think we can first share the code as is (asking extension author to copy and paste them) while waiting for the feedback from LSP. If LSP expressed interest to adopt the solution, we don't need to publish it anymore. If there is very slow (or no) progress on LSP, we can publish and maintain that. (There is some internal process we need to publish if we want to publish to an org under Google).

@DanTup
DanTup force-pushed the interactive-forms-as-lsl-client-feature branch from cf02498 to 4bed1ec Compare June 16, 2026 14:18
@DanTup
DanTup force-pushed the interactive-forms branch from 3c83172 to 28f3455 Compare June 16, 2026 14:23
@DanTup
DanTup force-pushed the interactive-forms-as-lsl-client-feature branch from c2ba60c to 8aa16bf Compare June 16, 2026 14:23
@DanTup
DanTup force-pushed the interactive-forms-as-lsl-client-feature branch from 8aa16bf to e6657d4 Compare June 16, 2026 14:39
@DanTup

DanTup commented Jun 16, 2026

Copy link
Copy Markdown
Member Author

/gemini review

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request refactors the interactive forms feature by converting the InteractiveLanguageClient subclass into a standalone StaticFeature named InteractiveFormsFeature, which is conditionally registered based on a new experimentalInteractiveForms configuration. The review feedback recommends wrapping the new conditional block in curly braces for consistency and removing several redundant type assertions in form.ts to clean up the code.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread src/extension/analysis/analyzer.ts
Comment thread src/extension/analysis/form.ts
Comment thread src/extension/analysis/form.ts
@DanTup

DanTup commented Jun 16, 2026

Copy link
Copy Markdown
Member Author

@h9jiang

(I don't think I can cast a vote though)

I think you should be able to review it. It currently says "@h9jiang
Requested changes" in the status for me. I did also just add you to the Collaborators list though (I think it sent you an invite), in case that gives you any better access to review.

I have moved the middleware back to the constructor and addressed some other minor issues (AI review feedback, and resolving some additional lints that fired when rebasing, since I'd upgraded some deps).

If this still looks good to you, I will merge it into the interactive-forms branch (not master) so I can do some additional testing/polishing before merging it into master, and will CC you on PRs if I make any further changes.

There is also a PR open at #6077 with some questions around handling of optional values that I'd like your input on (which may be worth resolving before you take a copy of form.ts - or you can take it as-is and just sync it periodically if there are changes).

I will also open an issue to discuss validation shortly, because I think having some kind of client-side validation would make the experience much nicer (the problem with showWarningMessage is that we cannot dismiss it even when the user corrects the problem).

@h9jiang h9jiang left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This LGTM.

@h9jiang

h9jiang commented Jun 16, 2026

Copy link
Copy Markdown
Collaborator

There is also a PR open at #6077 with some questions around handling of optional values that I'd like your input on (which may be worth resolving before you take a copy of form.ts - or you can take it as-is and just sync it periodically if there are changes).

Thanks. I replied last week. But the problem is, I don't know how to use github review... I write my comment and click comment button. But you can not see it until I click submit review..... For days I thought I already replied, it turns out it is just in "draft"...

Sorry for that.

@DanTup

DanTup commented Jun 16, 2026

Copy link
Copy Markdown
Member Author

Thanks!

Thanks. I replied last week. But the problem is, I don't know how to use github review... I write my comment and click comment button. But you can not see it until I click submit review.....

Ah, lol. No worries. You are definitely not alone in this (although, I sometimes I do the same in Gerrit too and only notice when it shows up in "Has draft comments" at the top of the dashboard 😆).

I'll merge this one and I'll review your comments on the other one properly tomorrow - thanks!

@DanTup
DanTup merged commit b27254f into interactive-forms Jun 16, 2026
23 of 24 checks passed
@DanTup
DanTup deleted the interactive-forms-as-lsl-client-feature branch June 16, 2026 19:07
@DanTup DanTup modified the milestones: v3.138.0, v3.140.0 Jun 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants