- Notifications
You must be signed in to change notification settings - Fork768
Revamp user preferences serialization/deserialization#2272
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.
Already on GitHub?Sign in to your account
base:main
Are you sure you want to change the base?
Uh oh!
There was an error while loading.Please reload this page.
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
Pull request overview
This PR refactors the user preferences system to use reflection-based serialization/deserialization with struct tags, replacing the error-prone switch-case approach. The changes make preferences immutable, support direct JSON marshaling/unmarshaling, and simplify configuration by mapping TypeScript config paths to struct fields viapref tags.
Key changes:
- Implements reflection-based parsing using
prefstruct tags to map VS Code config paths to fields - Adds custom type parsers and serializers for enum-like types (Tristate, OrganizeImportsCollation, etc.)
- Makes preferences immutable by removing mutation operations and returning the same pointer where possible
- Adds generic
deepCopyimplementation for creating copies when needed
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
internal/ls/lsutil/userpreferences.go | Core refactor: adds struct tags, reflection-based parsing/serialization, type parsers/serializers, and immutable Copy() implementation |
internal/ls/lsutil/userpreferences_test.go | New roundtrip test validating JSON marshaling/unmarshaling and parseWorker |
internal/project/snapshot.go | Removes unnecessary Copy() call when assigning immutable preferences |
internal/project/session.go | Removes Copy() calls and returns direct pointer to immutable preferences |
internal/lsp/server.go | Updates to use ParseUserPreferences() function and DefaultUserPreferences; fixes typo |
internal/ls/autoimports.go | Updates field access to use nested ModuleSpecifier struct |
internal/fourslash/fourslash.go | Updates to use DefaultUserPreferences directly instead of calling constructor |
| casereflect.Int: | ||
| returnint(field.Int()) | ||
| casereflect.String: | ||
| returnfield.String() |
CopilotAIDec 8, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
TheserializeField function will serialize empty string values for string-based enums (likeQuotePreference,JsxAttributeCompletionStyle, andIncludePackageJsonAutoImports) when they have their "Unknown" zero value. This causes these fields to be included in the serialized JSON as empty strings rather than being omitted.
Consider either:
- Adding a check to return
nilfor empty string values in thereflect.Stringcase - Adding custom serializers for these types (similar to
IncludeInlayParameterNameHintswhich handles empty strings)
This would prevent empty/zero values from polluting the serialized output.
| returnfield.String() | |
| s:=field.String() | |
| ifs=="" { | |
| returnnil | |
| } | |
| returns |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
Do we need some sort ofomitzero?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
Eh, not sure. The roundtripping is not perfect because these types were switched away from things like*string for convenience.
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.
| PreferTypeOnlyAutoImportsbool// !!! | ||
| ModuleSpecifierModuleSpecifierUserPreferences | ||
| IncludePackageJsonAutoImportsIncludePackageJsonAutoImports`pref:"preferences.includePackageJsonAutoImports"`// !!! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
Do you think it would be better to just omit these until the code is used?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
IIRC this is upcoming, so it would just be churn to remove I think...
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.
| ifname=="unstable" { | ||
| continue | ||
| } | ||
| // Skip known VS Code config sections that are handled by path-based parsing | ||
| switchname { | ||
| case"autoImports": | ||
| p.set("includeCompletionsForModuleExports",value) | ||
| case"objectLiteralMethodSnippets": | ||
| ifv,ok:=value.(map[string]any);ok { | ||
| p.set("includeCompletionsWithObjectLiteralMethodSnippets",parseEnabledBool(v)) | ||
| } | ||
| case"classMemberSnippets": | ||
| ifv,ok:=value.(map[string]any);ok { | ||
| p.set("includeCompletionsWithClassMemberSnippets",parseEnabledBool(v)) | ||
| case"preferences","suggest","inlayHints","referencesCodeLens", | ||
| "implementationsCodeLens","workspaceSymbols","format","tsserver","tsc","experimental": | ||
| continue |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
What's an example of a path that doesn't need to be skipped?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
Yeah, I think this is actually dead code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
Ah, so this was here to roundtrip unstable settings, but it isn't doing that right.
I'm going to rethink unstable settings. Probably just make them explicit.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
Okay, so the sort of problem is that we used to allow user prefs from like, anywhere, so you could have a object with say,{ "quotePreference": ... } and that would be read in, which is a validUserPreferences object. But, now over LSP, we are expecting things overtypescript. So it's maybe a question of what non-vscode people do.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
And, VS Code calls itquoteStyle, while preferences called itquotePreference, so that's uh, great
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
I split things apart so that we can both have the raw JS name we used to have, but also the LSP-based preference name.
| casereflect.Int: | ||
| returnint(field.Int()) | ||
| casereflect.String: | ||
| returnfield.String() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
Do we need some sort ofomitzero?
Uh oh!
There was an error while loading.Please reload this page.
| prefCopy.AutoImportFileExcludePatterns=slices.Clone(p.AutoImportFileExcludePatterns) | ||
| return&prefCopy | ||
| typefieldInfostruct { | ||
| rawNamestring// raw name for unstable section lookup (e.g., "quotePreference") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
So is the idea that to get to this, a user can only write the explicit path ortypescript.unstable.quotePreference?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
Yeah. Either, you are usingunstable and you were working by spread directly intoUserPreferences in Strada, or you were using the "real" name VS Code had, which takes precedence via the spread.
Arguably, this means we should entirely delete any unstable thing with a real name, though, because VS Code actually entirely overwrote these (by nature ofconfig.get having defaults):https://github.com/microsoft/vscode/blob/f9e3a710d84879d41431e7c17d8a8fe8612dfb26/extensions/typescript-language-features/src/languageFeatures/fileConfigurationManager.ts#L186
So maybe I went overkill and we shouldn't allow both names.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
Basically, it's hard to balance "LSP VS Code style settings" with "the old tsserver UserPreferences", defaults, spread, and then maybe even other editors...
So, I don't know what is exactly right.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
I think in this case, needing to support all theseunstable settings years later kind of defeats the purpose - but we'll probably need to support newunstable properties again anyway, right? So I'm not against keeping support around for it as a concept, and it is virtually no work for us to keep the current ones going if we want 6.0/7.0 to be smoother.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
(So your call but I think this is fine)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
Probably I'll just keep this.
jakebailey commentedDec 9, 2025
I think I'm going to wait for the other two user pref related PRs to finish and just fix this PR instead of forcing that on everyone. |
Uh oh!
There was an error while loading.Please reload this page.
In working on improving the fourslash testing, I wanted to make user preferences serializable, such that we can directly go from JSON to prefs and back, and therefore also test out the "actual" JSON encoding.
Making that work sort of snowballed into a total refactor, which eliminates the error-prone switch case for decoding, replacing it with struct tags and reflect.
Prefs now look like this:
A struct tag gives the name for the JSON propery (used by Strada), and also the name over LSP.https://github.com/microsoft/vscode/blob/main/extensions/typescript-language-features/package.json.
All prefs can still be set by the
unstableobject (which happens first, based on the raw name).Structs can be nested, e.g.:
And, for prefs that are actually defined elsewhere (like the above), conversion works so long as the structure is the same, making this less error prone:
UserPreferencesare also now considered to be immutable. They should never be mutated for any reason.Copy()can be used if you need to make a deep copy. Copying is used for a test, but also for cloning the defaults to use during unmarshalling. Nothing else needs to mutate any preferences.