gluon-lang/lsp-types

Don't assume enums are exhaustive

Closed this issue · 7 comments

We currently have code like this:

#[derive(Debug, Eq, PartialEq, Clone, Deserialize_repr, Serialize_repr)]
#[repr(u8)]
pub enum SignatureHelpTriggerKind {
    Invoked = 1,
    TriggerCharacter = 2,
    ContentChange = 3,
}

the problem is that if the client implements a protocol extension and send 4, this will completely prevent deserialization, instead of graceful degradation. We should do the following instead

#[serde(transparent)]
pub struct SignatureHelpTriggerKind(u8);
impl SignatureHelpTriggerKind {
    pub const Invoked = SignatureHelpTriggerKind(1);
    pub const TriggerCharacter = SignatureHelpTriggerKind(2);
    pub const ContentChange = SignatureHelpTriggerKind(3);
}

We already do this for some string-based enums:

impl SemanticTokenType {

As reported in rust-lang/rust-analyzer#8729 (comment), I recently explored making rust-analyzer compatible with Visual Studio 2022. These are the smallest changes that I had to do to lsp-types to make it compatible with Visual Studio: Quick and dirty fix to enums that are not compatible with Visual Studio.
There are two issues that arise when using rust-analyzer with VS:

  • Visual Studio adds some custom values for existing LSP enums, I worked around that adding a #[serde(other)] value to these enums,
  • Visual Studio uses some negative or very large numbers for internal values of LSP enums, I had to change some of them from u8 to i32.

I would like to work on this one. Should I update all the #[repr(u8)] enums?

That sounds like a good plan.

Short term, I think doing refactor like the one in #213 (comment) is the way to go.

Long-term, someone has to write lsp.d.ts file along the lines of vscode.d.ts, so that we can just generate the whole thing instead of manually duplicating the info from markdown files.

I think it needs:

  • manual or semi-automated "parsing" of LSP markdown spec into lsp.d.ts file
  • writing some TypeScript code to parse that d.ts and to emit some language-independent JSON (more or less serializing TS AST into JSON)
  • writing some Rust code to convert from JSON to serde structs

Ideally, this should be done in the protocel iteslf, but there was very little motion on this upstream: microsoft/language-server-protocol#67.

So, organizationally, I think it makes most sense for us to do all the work ourselves, and than just ask upstream if they are willing to pull.

One thing I notice is that most "extensible" enums (except DiagnosticTag?) are missing a corresponding type export, e.g.:

export namespace CompletionItemKind {
	export const Text = 1;
	export const Method = 2;
	export const Function = 3;
	export const Constructor = 4;
	export const Field = 5;
	export const Variable = 6;
	export const Class = 7;
	export const Interface = 8;
	export const Module = 9;
	export const Property = 10;
	export const Unit = 11;
	export const Value = 12;
	export const Enum = 13;
	export const Keyword = 14;
	export const Snippet = 15;
	export const Color = 16;
	export const File = 17;
	export const Reference = 18;
	export const Folder = 19;
	export const EnumMember = 20;
	export const Constant = 21;
	export const Struct = 22;
	export const Event = 23;
	export const Operator = 24;
	export const TypeParameter = 25;
}

// vs.

export namespace InsertTextMode {
	export const asIs: 1 = 1;
	export const adjustIndentation: 2 = 2;
}

export type InsertTextMode = 1 | 2;

But maybe that's just a coincidence.

Couldn't we have just kept the old enums and tagged them with #[non_exhaustive]?

@archseer not really. First of all, it would prevent users from defining protocol extensions, because you wouldn't be able to make new enum variants. Second, I don't know if serde knows can deal with those. What might have worked was something like:

#[repr(u8)]
enum Foo {
    Bar = 1,
    Baz = 2,
    #[serde(other)]
    Extension(u8) = 3,
}

but I don't think #[serde(other)] works like that, and custom discriminants on tuples isn't stable anyway.