vendor tsgo
This commit is contained in:
2
tools/tsgo/internal/lsp/lsproto/_generate/.gitignore
vendored
Normal file
2
tools/tsgo/internal/lsp/lsproto/_generate/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
metaModel.json
|
||||
metaModel.schema.json
|
||||
39
tools/tsgo/internal/lsp/lsproto/_generate/fetchModel.mts
Normal file
39
tools/tsgo/internal/lsp/lsproto/_generate/fetchModel.mts
Normal file
@@ -0,0 +1,39 @@
|
||||
#!/usr/bin/env -S node --experimental-strip-types
|
||||
|
||||
// Usage: node --experimental-strip-types fetchModel.mts
|
||||
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import url from "node:url";
|
||||
|
||||
const __filename = url.fileURLToPath(new URL(import.meta.url));
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
const metaModelPath = path.join(__dirname, "metaModel.json");
|
||||
const metaModelSchemaPath = path.join(__dirname, "metaModelSchema.mts");
|
||||
|
||||
// Resolve the vscode-languageclient version from the root package-lock.json.
|
||||
const lockfilePath = path.resolve(__dirname, "../../../../package-lock.json");
|
||||
const lockfile = JSON.parse(fs.readFileSync(lockfilePath, "utf-8"));
|
||||
const clientVersion: string = lockfile.packages["node_modules/vscode-languageclient"].version;
|
||||
|
||||
const ref = `release/client/${clientVersion}`;
|
||||
console.log(`Using vscode-languageclient@${clientVersion}`);
|
||||
|
||||
const metaModelURL = `https://raw.githubusercontent.com/microsoft/vscode-languageserver-node/${ref}/protocol/metaModel.json`;
|
||||
const metaModelSchemaURL = `https://raw.githubusercontent.com/microsoft/vscode-languageserver-node/${ref}/tools/src/metaModel.ts`;
|
||||
|
||||
const metaModelResponse = await fetch(metaModelURL);
|
||||
const metaModel = await metaModelResponse.text();
|
||||
fs.writeFileSync(metaModelPath, metaModel);
|
||||
|
||||
const metaModelSchemaResponse = await fetch(metaModelSchemaURL);
|
||||
let metaModelSchema = await metaModelSchemaResponse.text();
|
||||
|
||||
// Patch the schema to add omitzeroValue property to Property type
|
||||
metaModelSchema = metaModelSchema.replace(
|
||||
/(\t \* Whether the property is deprecated or not\. If deprecated\n\t \* the property contains the deprecation message\.\n\t \*\/\n\tdeprecated\?: string;)\n}/m,
|
||||
`$1\n\n\t/**\n\t * Whether this property uses omitzero without being a pointer.\n\t * Custom extension for special value types.\n\t */\n\tomitzeroValue?: boolean;\n}`,
|
||||
);
|
||||
|
||||
fs.writeFileSync(metaModelSchemaPath, metaModelSchema);
|
||||
3436
tools/tsgo/internal/lsp/lsproto/_generate/generate.mts
Normal file
3436
tools/tsgo/internal/lsp/lsproto/_generate/generate.mts
Normal file
File diff suppressed because it is too large
Load Diff
635
tools/tsgo/internal/lsp/lsproto/_generate/metaModelSchema.mts
Normal file
635
tools/tsgo/internal/lsp/lsproto/_generate/metaModelSchema.mts
Normal file
@@ -0,0 +1,635 @@
|
||||
/* --------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
* ------------------------------------------------------------------------------------------ */
|
||||
|
||||
export type BaseTypes = 'URI' | 'DocumentUri' | 'integer' | 'uinteger' | 'decimal' | 'RegExp' | 'string' | 'boolean' | 'null';
|
||||
|
||||
export type TypeKind = 'base' | 'reference' | 'array' | 'map' | 'and' | 'or' | 'tuple' | 'literal' | 'stringLiteral' | 'integerLiteral' | 'booleanLiteral';
|
||||
|
||||
/**
|
||||
* Indicates in which direction a message is sent in the protocol.
|
||||
*/
|
||||
export type MessageDirection = 'clientToServer' | 'serverToClient' | 'both';
|
||||
|
||||
/**
|
||||
* Represents a base type like `string` or `DocumentUri`.
|
||||
*/
|
||||
export type BaseType = {
|
||||
kind: 'base';
|
||||
name: BaseTypes;
|
||||
};
|
||||
|
||||
/**
|
||||
* Represents a reference to another type (e.g. `TextDocument`).
|
||||
* This is either a `Structure`, a `Enumeration` or a `TypeAlias`
|
||||
* in the same meta model.
|
||||
*/
|
||||
export type ReferenceType = {
|
||||
kind: 'reference';
|
||||
name: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Represents an array type (e.g. `TextDocument[]`).
|
||||
*/
|
||||
export type ArrayType = {
|
||||
kind: 'array';
|
||||
element: Type;
|
||||
};
|
||||
|
||||
/**
|
||||
* Represents a type that can be used as a key in a
|
||||
* map type. If a reference type is used then the
|
||||
* type must either resolve to a `string` or `integer`
|
||||
* type. (e.g. `type ChangeAnnotationIdentifier === string`).
|
||||
*/
|
||||
export type MapKeyType = { kind: 'base'; name: 'URI' | 'DocumentUri' | 'string' | 'integer' } | ReferenceType;
|
||||
|
||||
/**
|
||||
* Represents a JSON object map
|
||||
* (e.g. `interface Map<K extends string | integer, V> { [key: K] => V; }`).
|
||||
*/
|
||||
export type MapType = {
|
||||
kind: 'map';
|
||||
key: MapKeyType;
|
||||
value: Type;
|
||||
};
|
||||
|
||||
/**
|
||||
* Represents an `and` type
|
||||
* (e.g. TextDocumentParams & WorkDoneProgressParams`).
|
||||
*/
|
||||
export type AndType = {
|
||||
kind: 'and';
|
||||
items: Type[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Represents an `or` type
|
||||
* (e.g. `Location | LocationLink`).
|
||||
*/
|
||||
export type OrType = {
|
||||
kind: 'or';
|
||||
items: Type[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Represents a `tuple` type
|
||||
* (e.g. `[integer, integer]`).
|
||||
*/
|
||||
export type TupleType = {
|
||||
kind: 'tuple';
|
||||
items: Type[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Represents a literal structure
|
||||
* (e.g. `property: { start: uinteger; end: uinteger; }`).
|
||||
*/
|
||||
export type StructureLiteralType = {
|
||||
kind: 'literal';
|
||||
value: StructureLiteral;
|
||||
};
|
||||
|
||||
/**
|
||||
* Represents a string literal type
|
||||
* (e.g. `kind: 'rename'`).
|
||||
*/
|
||||
export type StringLiteralType = {
|
||||
kind: 'stringLiteral';
|
||||
value: string;
|
||||
};
|
||||
|
||||
export type IntegerLiteralType = {
|
||||
/**
|
||||
* Represents an integer literal type
|
||||
* (e.g. `kind: 1`).
|
||||
*/
|
||||
kind: 'integerLiteral';
|
||||
value: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Represents a boolean literal type
|
||||
* (e.g. `kind: true`).
|
||||
*/
|
||||
export type BooleanLiteralType = {
|
||||
kind: 'booleanLiteral';
|
||||
value: boolean;
|
||||
};
|
||||
|
||||
export type Type = BaseType | ReferenceType | ArrayType | MapType | AndType | OrType | TupleType | StructureLiteralType | StringLiteralType | IntegerLiteralType | BooleanLiteralType;
|
||||
|
||||
/**
|
||||
* Represents a LSP request
|
||||
*/
|
||||
export type Request = {
|
||||
/**
|
||||
* The request's method name.
|
||||
*/
|
||||
method: string;
|
||||
|
||||
/**
|
||||
* The type name of the request if any.
|
||||
*/
|
||||
typeName?: string;
|
||||
|
||||
/**
|
||||
* The parameter type(s) if any.
|
||||
*/
|
||||
params?: Type | Type[];
|
||||
|
||||
/**
|
||||
* The result type.
|
||||
*/
|
||||
result: Type;
|
||||
|
||||
/**
|
||||
* Optional partial result type if the request
|
||||
* supports partial result reporting.
|
||||
*/
|
||||
partialResult?: Type;
|
||||
|
||||
/**
|
||||
* An optional error data type.
|
||||
*/
|
||||
errorData?: Type;
|
||||
|
||||
/**
|
||||
* Optional a dynamic registration method if it
|
||||
* different from the request's method.
|
||||
*/
|
||||
registrationMethod?: string;
|
||||
|
||||
/**
|
||||
* Optional registration options if the request
|
||||
* supports dynamic registration.
|
||||
*/
|
||||
registrationOptions?: Type;
|
||||
|
||||
/**
|
||||
* The direction in which this request is sent
|
||||
* in the protocol.
|
||||
*/
|
||||
messageDirection: MessageDirection;
|
||||
|
||||
/**
|
||||
* An optional documentation;
|
||||
*/
|
||||
documentation?: string;
|
||||
|
||||
/**
|
||||
* Since when (release number) this request is
|
||||
* available. Is undefined if not known.
|
||||
*/
|
||||
since?: string;
|
||||
|
||||
/**
|
||||
* All since tags in case there was more than one tag.
|
||||
* Is undefined if not known.
|
||||
*/
|
||||
sinceTags?: string[];
|
||||
|
||||
/**
|
||||
* Whether this is a proposed feature. If omitted
|
||||
* the feature is final.
|
||||
*/
|
||||
proposed?: boolean;
|
||||
|
||||
/**
|
||||
* Whether the request is deprecated or not. If deprecated
|
||||
* the property contains the deprecation message.
|
||||
*/
|
||||
deprecated?: string;
|
||||
|
||||
/**
|
||||
* The client capability property path if any.
|
||||
*/
|
||||
clientCapability?: string;
|
||||
|
||||
/**
|
||||
* The server capability property path if any.
|
||||
*/
|
||||
serverCapability?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Represents a LSP notification
|
||||
*/
|
||||
export type Notification = {
|
||||
/**
|
||||
* The notifications's method name.
|
||||
*/
|
||||
method: string;
|
||||
|
||||
/**
|
||||
* The type name of the notifications if any.
|
||||
*/
|
||||
typeName?: string;
|
||||
|
||||
/**
|
||||
* The parameter type(s) if any.
|
||||
*/
|
||||
params?: Type | Type[];
|
||||
|
||||
/**
|
||||
* Optional a dynamic registration method if it
|
||||
* different from the notifications's method.
|
||||
*/
|
||||
registrationMethod?: string;
|
||||
|
||||
/**
|
||||
* Optional registration options if the notification
|
||||
* supports dynamic registration.
|
||||
*/
|
||||
registrationOptions?: Type;
|
||||
|
||||
/**
|
||||
* The direction in which this notification is sent
|
||||
* in the protocol.
|
||||
*/
|
||||
messageDirection: MessageDirection;
|
||||
|
||||
/**
|
||||
* An optional documentation;
|
||||
*/
|
||||
documentation?: string;
|
||||
|
||||
/**
|
||||
* Since when (release number) this notification is
|
||||
* available. Is undefined if not known.
|
||||
*/
|
||||
since?: string;
|
||||
|
||||
/**
|
||||
* All since tags in case there was more than one tag.
|
||||
* Is undefined if not known.
|
||||
*/
|
||||
sinceTags?: string[];
|
||||
|
||||
/**
|
||||
* Whether this is a proposed notification. If omitted
|
||||
* the notification is final.
|
||||
*/
|
||||
proposed?: boolean;
|
||||
|
||||
/**
|
||||
* Whether the notification is deprecated or not. If deprecated
|
||||
* the property contains the deprecation message.
|
||||
*/
|
||||
deprecated?: string;
|
||||
|
||||
/**
|
||||
* The client capability property path if any.
|
||||
*/
|
||||
clientCapability?: string;
|
||||
|
||||
/**
|
||||
* The server capability property path if any.
|
||||
*/
|
||||
serverCapability?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Represents an object property.
|
||||
*/
|
||||
export type Property = {
|
||||
/**
|
||||
* The property name;
|
||||
*/
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* The type of the property
|
||||
*/
|
||||
type: Type;
|
||||
|
||||
/**
|
||||
* Whether the property is optional. If
|
||||
* omitted, the property is mandatory.
|
||||
*/
|
||||
optional?: boolean;
|
||||
|
||||
/**
|
||||
* An optional documentation.
|
||||
*/
|
||||
documentation?: string;
|
||||
|
||||
/**
|
||||
* Since when (release number) this property is
|
||||
* available. Is undefined if not known.
|
||||
*/
|
||||
since?: string;
|
||||
|
||||
/**
|
||||
* All since tags in case there was more than one tag.
|
||||
* Is undefined if not known.
|
||||
*/
|
||||
sinceTags?: string[];
|
||||
|
||||
/**
|
||||
* Whether this is a proposed property. If omitted,
|
||||
* the structure is final.
|
||||
*/
|
||||
proposed?: boolean;
|
||||
|
||||
/**
|
||||
* Whether the property is deprecated or not. If deprecated
|
||||
* the property contains the deprecation message.
|
||||
*/
|
||||
deprecated?: string;
|
||||
|
||||
/**
|
||||
* Whether this property uses omitzero without being a pointer.
|
||||
* Custom extension for special value types.
|
||||
*/
|
||||
omitzeroValue?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Defines the structure of an object literal.
|
||||
*/
|
||||
export type Structure = {
|
||||
/**
|
||||
* The name of the structure.
|
||||
*/
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* Structures extended from. This structures form
|
||||
* a polymorphic type hierarchy.
|
||||
*/
|
||||
extends?: Type[];
|
||||
|
||||
/**
|
||||
* Structures to mix in. The properties of these
|
||||
* structures are `copied` into this structure.
|
||||
* Mixins don't form a polymorphic type hierarchy in
|
||||
* LSP.
|
||||
*/
|
||||
mixins?: Type[];
|
||||
|
||||
/**
|
||||
* The properties.
|
||||
*/
|
||||
properties: Property[];
|
||||
|
||||
/**
|
||||
* An optional documentation;
|
||||
*/
|
||||
documentation?: string;
|
||||
|
||||
/**
|
||||
* Since when (release number) this structure is
|
||||
* available. Is undefined if not known.
|
||||
*/
|
||||
since?: string;
|
||||
|
||||
/**
|
||||
* All since tags in case there was more than one tag.
|
||||
* Is undefined if not known.
|
||||
*/
|
||||
sinceTags?: string[];
|
||||
|
||||
/**
|
||||
* Whether this is a proposed structure. If omitted,
|
||||
* the structure is final.
|
||||
*/
|
||||
proposed?: boolean;
|
||||
|
||||
/**
|
||||
* Whether the structure is deprecated or not. If deprecated
|
||||
* the property contains the deprecation message.
|
||||
*/
|
||||
deprecated?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Defines an unnamed structure of an object literal.
|
||||
*/
|
||||
export type StructureLiteral = {
|
||||
|
||||
/**
|
||||
* The properties.
|
||||
*/
|
||||
properties: Property[];
|
||||
|
||||
/**
|
||||
* An optional documentation.
|
||||
*/
|
||||
documentation?: string;
|
||||
|
||||
/**
|
||||
* Since when (release number) this structure is
|
||||
* available. Is undefined if not known.
|
||||
*/
|
||||
since?: string;
|
||||
|
||||
/**
|
||||
* All since tags in case there was more than one tag.
|
||||
* Is undefined if not known.
|
||||
*/
|
||||
sinceTags?: string[];
|
||||
|
||||
/**
|
||||
* Whether this is a proposed structure. If omitted,
|
||||
* the structure is final.
|
||||
*/
|
||||
proposed?: boolean;
|
||||
|
||||
/**
|
||||
* Whether the literal is deprecated or not. If deprecated
|
||||
* the property contains the deprecation message.
|
||||
*/
|
||||
deprecated?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Defines a type alias.
|
||||
* (e.g. `type Definition = Location | LocationLink`)
|
||||
*/
|
||||
export type TypeAlias = {
|
||||
/**
|
||||
* The name of the type alias.
|
||||
*/
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* The aliased type.
|
||||
*/
|
||||
type: Type;
|
||||
|
||||
/**
|
||||
* An optional documentation.
|
||||
*/
|
||||
documentation?: string;
|
||||
|
||||
/**
|
||||
* Since when (release number) this structure is
|
||||
* available. Is undefined if not known.
|
||||
*/
|
||||
since?: string;
|
||||
|
||||
/**
|
||||
* All since tags in case there was more than one tag.
|
||||
* Is undefined if not known.
|
||||
*/
|
||||
sinceTags?: string[];
|
||||
|
||||
/**
|
||||
* Whether this is a proposed type alias. If omitted,
|
||||
* the type alias is final.
|
||||
*/
|
||||
proposed?: boolean;
|
||||
|
||||
/**
|
||||
* Whether the type alias is deprecated or not. If deprecated
|
||||
* the property contains the deprecation message.
|
||||
*/
|
||||
deprecated?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Defines an enumeration entry.
|
||||
*/
|
||||
export type EnumerationEntry = {
|
||||
/**
|
||||
* The name of the enum item.
|
||||
*/
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* The value.
|
||||
*/
|
||||
value: string | number;
|
||||
|
||||
/**
|
||||
* An optional documentation.
|
||||
*/
|
||||
documentation?: string;
|
||||
|
||||
/**
|
||||
* Since when (release number) this enumeration entry is
|
||||
* available. Is undefined if not known.
|
||||
*/
|
||||
since?: string;
|
||||
|
||||
/**
|
||||
* All since tags in case there was more than one tag.
|
||||
* Is undefined if not known.
|
||||
*/
|
||||
sinceTags?: string[];
|
||||
|
||||
/**
|
||||
* Whether this is a proposed enumeration entry. If omitted,
|
||||
* the enumeration entry is final.
|
||||
*/
|
||||
proposed?: boolean;
|
||||
|
||||
/**
|
||||
* Whether the enum entry is deprecated or not. If deprecated
|
||||
* the property contains the deprecation message.
|
||||
*/
|
||||
deprecated?: string;
|
||||
};
|
||||
|
||||
export type EnumerationType = { kind: 'base'; name: 'string' | 'integer' | 'uinteger' };
|
||||
|
||||
/**
|
||||
* Defines an enumeration.
|
||||
*/
|
||||
export type Enumeration = {
|
||||
/**
|
||||
* The name of the enumeration.
|
||||
*/
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* The type of the elements.
|
||||
*/
|
||||
type: EnumerationType;
|
||||
|
||||
/**
|
||||
* The enum values.
|
||||
*/
|
||||
values: EnumerationEntry[];
|
||||
|
||||
/**
|
||||
* Whether the enumeration supports custom values (e.g. values which are not
|
||||
* part of the set defined in `values`). If omitted no custom values are
|
||||
* supported.
|
||||
*/
|
||||
supportsCustomValues?: boolean;
|
||||
|
||||
/**
|
||||
* An optional documentation.
|
||||
*/
|
||||
documentation?: string;
|
||||
|
||||
/**
|
||||
* Since when (release number) this enumeration is
|
||||
* available. Is undefined if not known.
|
||||
*/
|
||||
since?: string;
|
||||
|
||||
/**
|
||||
* All since tags in case there was more than one tag.
|
||||
* Is undefined if not known.
|
||||
*/
|
||||
sinceTags?: string[];
|
||||
|
||||
/**
|
||||
* Whether this is a proposed enumeration. If omitted,
|
||||
* the enumeration is final.
|
||||
*/
|
||||
proposed?: boolean;
|
||||
|
||||
/**
|
||||
* Whether the enumeration is deprecated or not. If deprecated
|
||||
* the property contains the deprecation message.
|
||||
*/
|
||||
deprecated?: string;
|
||||
};
|
||||
|
||||
export type MetaData = {
|
||||
/**
|
||||
* The protocol version.
|
||||
*/
|
||||
version: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* The actual meta model.
|
||||
*/
|
||||
export type MetaModel = {
|
||||
/**
|
||||
* Additional meta data.
|
||||
*/
|
||||
metaData: MetaData;
|
||||
|
||||
/**
|
||||
* The requests.
|
||||
*/
|
||||
requests: Request[];
|
||||
|
||||
/**
|
||||
* The notifications.
|
||||
*/
|
||||
notifications: Notification[];
|
||||
|
||||
/**
|
||||
* The structures.
|
||||
*/
|
||||
structures: Structure[];
|
||||
|
||||
/**
|
||||
* The enumerations.
|
||||
*/
|
||||
enumerations: Enumeration[];
|
||||
|
||||
/**
|
||||
* The type aliases.
|
||||
*/
|
||||
typeAliases: TypeAlias[];
|
||||
};
|
||||
20
tools/tsgo/internal/lsp/lsproto/_generate/tsconfig.json
Normal file
20
tools/tsgo/internal/lsp/lsproto/_generate/tsconfig.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"module": "NodeNext",
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"allowImportingTsExtensions": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"types": ["node"],
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
},
|
||||
"include": [
|
||||
"*.mts",
|
||||
"*.mjs"
|
||||
]
|
||||
}
|
||||
33
tools/tsgo/internal/lsp/lsproto/baseproto.go
Normal file
33
tools/tsgo/internal/lsp/lsproto/baseproto.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package lsproto
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/jsonrpc"
|
||||
)
|
||||
|
||||
// https://microsoft.github.io/language-server-protocol/specifications/base/0.9/specification/
|
||||
|
||||
// BaseReader wraps jsonrpc.Reader for backwards compatibility.
|
||||
type BaseReader struct {
|
||||
*jsonrpc.Reader
|
||||
}
|
||||
|
||||
// NewBaseReader creates a new BaseReader.
|
||||
func NewBaseReader(r io.Reader) *BaseReader {
|
||||
return &BaseReader{
|
||||
Reader: jsonrpc.NewReader(r),
|
||||
}
|
||||
}
|
||||
|
||||
// BaseWriter wraps jsonrpc.Writer for backwards compatibility.
|
||||
type BaseWriter struct {
|
||||
*jsonrpc.Writer
|
||||
}
|
||||
|
||||
// NewBaseWriter creates a new BaseWriter.
|
||||
func NewBaseWriter(w io.Writer) *BaseWriter {
|
||||
return &BaseWriter{
|
||||
Writer: jsonrpc.NewWriter(w),
|
||||
}
|
||||
}
|
||||
151
tools/tsgo/internal/lsp/lsproto/baseproto_test.go
Normal file
151
tools/tsgo/internal/lsp/lsproto/baseproto_test.go
Normal file
@@ -0,0 +1,151 @@
|
||||
package lsproto_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
func TestBaseReader(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
input []byte
|
||||
value []byte
|
||||
err string
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
input: []byte("Content-Length: 0\r\n\r\n"),
|
||||
err: "jsonrpc: no content length",
|
||||
},
|
||||
{
|
||||
name: "early end",
|
||||
input: []byte("oops"),
|
||||
err: "EOF",
|
||||
},
|
||||
{
|
||||
name: "negative length",
|
||||
input: []byte("Content-Length: -1\r\n\r\n"),
|
||||
err: "jsonrpc: invalid content length: negative value -1",
|
||||
},
|
||||
{
|
||||
name: "invalid content",
|
||||
input: []byte("Content-Length: 1\r\n\r\n{"),
|
||||
value: []byte("{"),
|
||||
},
|
||||
{
|
||||
name: "valid content",
|
||||
input: []byte("Content-Length: 2\r\n\r\n{}"),
|
||||
value: []byte("{}"),
|
||||
},
|
||||
{
|
||||
name: "extra header values",
|
||||
input: []byte("Content-Length: 2\r\nExtra: 1\r\n\r\n{}"),
|
||||
value: []byte("{}"),
|
||||
},
|
||||
{
|
||||
name: "too long content length",
|
||||
input: []byte("Content-Length: 100\r\n\r\n{}"),
|
||||
err: "jsonrpc: read content: unexpected EOF",
|
||||
},
|
||||
{
|
||||
name: "missing content length",
|
||||
input: []byte("Content-Length: \r\n\r\n{}"),
|
||||
err: "jsonrpc: invalid content length: parse error: strconv.ParseInt: parsing \"\": invalid syntax",
|
||||
},
|
||||
{
|
||||
name: "invalid header",
|
||||
input: []byte("Nope\r\n\r\n{}"),
|
||||
err: "jsonrpc: invalid header: \"Nope\\r\\n\"",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
r := lsproto.NewBaseReader(bytes.NewReader(tt.input))
|
||||
|
||||
out, err := r.Read()
|
||||
if tt.err != "" {
|
||||
assert.Error(t, err, tt.err)
|
||||
}
|
||||
assert.DeepEqual(t, out, tt.value)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBaseReaderMultipleReads(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
data := []byte(
|
||||
"Content-Length: 4\r\n\r\n1234" +
|
||||
"Content-Length: 2\r\n\r\n{}",
|
||||
)
|
||||
r := lsproto.NewBaseReader(bytes.NewReader(data))
|
||||
|
||||
v1, err := r.Read()
|
||||
assert.NilError(t, err)
|
||||
assert.DeepEqual(t, v1, []byte("1234"))
|
||||
|
||||
v2, err := r.Read()
|
||||
assert.NilError(t, err)
|
||||
assert.DeepEqual(t, v2, []byte("{}"))
|
||||
|
||||
_, err = r.Read()
|
||||
assert.Error(t, err, "EOF")
|
||||
}
|
||||
|
||||
type errorReader struct{}
|
||||
|
||||
func (*errorReader) Read([]byte) (int, error) {
|
||||
return 0, errors.New("test error")
|
||||
}
|
||||
|
||||
func TestBaseWriter(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
value []byte
|
||||
input []byte
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
value: []byte("{}"),
|
||||
input: []byte("Content-Length: 2\r\n\r\n{}"),
|
||||
},
|
||||
{
|
||||
name: "bigger object",
|
||||
value: []byte("{\"key\":\"value\"}"),
|
||||
input: []byte("Content-Length: 15\r\n\r\n{\"key\":\"value\"}"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
var b bytes.Buffer
|
||||
w := lsproto.NewBaseWriter(&b)
|
||||
err := w.Write(tt.value)
|
||||
assert.NilError(t, err)
|
||||
assert.DeepEqual(t, b.Bytes(), tt.input)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBaseWriterWriteError(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
w := lsproto.NewBaseWriter(&errorWriter{})
|
||||
err := w.Write([]byte("{}"))
|
||||
assert.Error(t, err, "test error")
|
||||
}
|
||||
|
||||
type errorWriter struct{}
|
||||
|
||||
func (*errorWriter) Write([]byte) (int, error) {
|
||||
return 0, errors.New("test error")
|
||||
}
|
||||
130
tools/tsgo/internal/lsp/lsproto/jsonrpc.go
Normal file
130
tools/tsgo/internal/lsp/lsproto/jsonrpc.go
Normal file
@@ -0,0 +1,130 @@
|
||||
package lsproto
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/json"
|
||||
"github.com/microsoft/typescript-go/internal/jsonrpc"
|
||||
)
|
||||
|
||||
// NewID creates an ID from an IntegerOrString value.
|
||||
// This wrapper exists because lsproto has its own IntegerOrString type.
|
||||
func NewID(rawValue IntegerOrString) *jsonrpc.ID {
|
||||
if rawValue.String != nil {
|
||||
return jsonrpc.NewIDString(*rawValue.String)
|
||||
}
|
||||
return jsonrpc.NewIDInt(*rawValue.Integer)
|
||||
}
|
||||
|
||||
type Message struct {
|
||||
Kind jsonrpc.MessageKind
|
||||
msg any
|
||||
}
|
||||
|
||||
func (m *Message) AsRequest() *RequestMessage {
|
||||
return m.msg.(*RequestMessage)
|
||||
}
|
||||
|
||||
func (m *Message) AsResponse() *ResponseMessage {
|
||||
return m.msg.(*ResponseMessage)
|
||||
}
|
||||
|
||||
func (m *Message) UnmarshalJSON(data []byte) error {
|
||||
var raw struct {
|
||||
JSONRPC jsonrpc.JSONRPCVersion `json:"jsonrpc"`
|
||||
Method Method `json:"method"`
|
||||
ID *jsonrpc.ID `json:"id,omitzero"`
|
||||
Params json.Value `json:"params"`
|
||||
// We don't have a method in the response, so we have no idea what to decode.
|
||||
// Store the raw text and let the caller decode it.
|
||||
Result json.Value `json:"result,omitzero"`
|
||||
Error *jsonrpc.ResponseError `json:"error,omitzero"`
|
||||
}
|
||||
if err := json.Unmarshal(data, &raw); err != nil {
|
||||
return fmt.Errorf("%w: %w", ErrorCodeInvalidRequest, err)
|
||||
}
|
||||
if raw.ID != nil && raw.Method == "" {
|
||||
m.Kind = jsonrpc.MessageKindResponse
|
||||
m.msg = &ResponseMessage{
|
||||
ID: raw.ID,
|
||||
Result: raw.Result,
|
||||
Error: raw.Error,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var params any
|
||||
if len(raw.Params) > 0 {
|
||||
params = raw.Params
|
||||
}
|
||||
|
||||
if raw.ID == nil {
|
||||
m.Kind = jsonrpc.MessageKindNotification
|
||||
} else {
|
||||
m.Kind = jsonrpc.MessageKindRequest
|
||||
}
|
||||
|
||||
m.msg = &RequestMessage{
|
||||
ID: raw.ID,
|
||||
Method: raw.Method,
|
||||
Params: params,
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Message) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(m.msg)
|
||||
}
|
||||
|
||||
type RequestMessage struct {
|
||||
JSONRPC jsonrpc.JSONRPCVersion `json:"jsonrpc"`
|
||||
ID *jsonrpc.ID `json:"id,omitzero"`
|
||||
Method Method `json:"method"`
|
||||
Params any `json:"params,omitzero"`
|
||||
}
|
||||
|
||||
func (r *RequestMessage) Message() *Message {
|
||||
kind := jsonrpc.MessageKindRequest
|
||||
if r.ID == nil {
|
||||
kind = jsonrpc.MessageKindNotification
|
||||
}
|
||||
return &Message{
|
||||
Kind: kind,
|
||||
msg: r,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *RequestMessage) UnmarshalJSON(data []byte) error {
|
||||
var raw struct {
|
||||
JSONRPC jsonrpc.JSONRPCVersion `json:"jsonrpc"`
|
||||
ID *jsonrpc.ID `json:"id"`
|
||||
Method Method `json:"method"`
|
||||
Params json.Value `json:"params"`
|
||||
}
|
||||
if err := json.Unmarshal(data, &raw); err != nil {
|
||||
return fmt.Errorf("%w: %w", ErrorCodeInvalidRequest, err)
|
||||
}
|
||||
|
||||
r.ID = raw.ID
|
||||
r.Method = raw.Method
|
||||
if len(raw.Params) > 0 {
|
||||
r.Params = raw.Params
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type ResponseMessage struct {
|
||||
JSONRPC jsonrpc.JSONRPCVersion `json:"jsonrpc"`
|
||||
ID *jsonrpc.ID `json:"id"`
|
||||
Result any `json:"result,omitzero"`
|
||||
Error *jsonrpc.ResponseError `json:"error,omitzero"`
|
||||
}
|
||||
|
||||
func (r *ResponseMessage) Message() *Message {
|
||||
return &Message{
|
||||
Kind: jsonrpc.MessageKindResponse,
|
||||
msg: r,
|
||||
}
|
||||
}
|
||||
312
tools/tsgo/internal/lsp/lsproto/lsp.go
Normal file
312
tools/tsgo/internal/lsp/lsproto/lsp.go
Normal file
@@ -0,0 +1,312 @@
|
||||
package lsproto
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/bundled"
|
||||
"github.com/microsoft/typescript-go/internal/json"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/jsonrpc"
|
||||
"github.com/microsoft/typescript-go/internal/tspath"
|
||||
)
|
||||
|
||||
type DocumentUri string // !!!
|
||||
|
||||
func (uri DocumentUri) FileName() string {
|
||||
if bundled.IsBundled(string(uri)) {
|
||||
return string(uri)
|
||||
}
|
||||
if strings.HasPrefix(string(uri), "file://") {
|
||||
parsed, err := url.Parse(string(uri))
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("invalid file URI: %s", uri))
|
||||
}
|
||||
if parsed.Host != "" {
|
||||
return "//" + parsed.Host + parsed.Path
|
||||
}
|
||||
return fixWindowsURIPath(parsed.Path)
|
||||
}
|
||||
|
||||
// Leave all other URIs escaped so we can round-trip them.
|
||||
|
||||
scheme, path, ok := strings.Cut(string(uri), ":")
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("invalid URI: %s", uri))
|
||||
}
|
||||
|
||||
authority := "ts-nul-authority"
|
||||
if rest, ok := strings.CutPrefix(path, "//"); ok {
|
||||
authority, path, ok = strings.Cut(rest, "/")
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("invalid URI: %s", uri))
|
||||
}
|
||||
}
|
||||
|
||||
return "^/" + scheme + "/" + authority + "/" + path
|
||||
}
|
||||
|
||||
func (uri DocumentUri) Path(useCaseSensitiveFileNames bool) tspath.Path {
|
||||
fileName := uri.FileName()
|
||||
return tspath.ToPath(fileName, "", useCaseSensitiveFileNames)
|
||||
}
|
||||
|
||||
func fixWindowsURIPath(path string) string {
|
||||
if rest, ok := strings.CutPrefix(path, "/"); ok {
|
||||
if volume, rest, ok := tspath.SplitVolumePath(rest); ok {
|
||||
return volume + rest
|
||||
}
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
type HasTextDocumentURI interface {
|
||||
TextDocumentURI() DocumentUri
|
||||
}
|
||||
|
||||
type HasTextDocumentPosition interface {
|
||||
HasTextDocumentURI
|
||||
TextDocumentPosition() Position
|
||||
}
|
||||
|
||||
type HasLocations interface {
|
||||
GetLocations() *[]Location
|
||||
}
|
||||
|
||||
type HasLocation interface {
|
||||
GetLocation() Location
|
||||
}
|
||||
|
||||
type URI string // !!!
|
||||
|
||||
type Method string
|
||||
|
||||
func errNotObject(k json.Kind) error {
|
||||
return fmt.Errorf("expected object start, but encountered %v", k)
|
||||
}
|
||||
|
||||
func errNull(field string) error {
|
||||
return fmt.Errorf("null value is not allowed for field %q", field)
|
||||
}
|
||||
|
||||
func errMissing(props []string) error {
|
||||
return fmt.Errorf("missing required properties: %s", strings.Join(props, ", "))
|
||||
}
|
||||
|
||||
func errInvalidKind(typeName string, got json.Kind) error {
|
||||
return fmt.Errorf("invalid %s: got %v", typeName, got)
|
||||
}
|
||||
|
||||
func errInvalidValue(typeName string, data []byte) error {
|
||||
return fmt.Errorf("invalid %s: %s", typeName, data)
|
||||
}
|
||||
|
||||
func errLiteralMismatch(typeName string, expected string, got []byte) error {
|
||||
return fmt.Errorf("expected %s value %s, got %s", typeName, expected, got)
|
||||
}
|
||||
|
||||
func assertOnlyOne(message string, count int) {
|
||||
if count != 1 {
|
||||
panic(message)
|
||||
}
|
||||
}
|
||||
|
||||
func assertAtMostOne(message string, count int) {
|
||||
if count > 1 {
|
||||
panic(message)
|
||||
}
|
||||
}
|
||||
|
||||
// jsonKeyCheck compares a raw JSON key token (including quotes) against a Go string.
|
||||
func jsonKeyCheck(name []byte, key string) bool {
|
||||
return len(name) == len(key)+2 && name[0] == '"' && string(name[1:len(name)-1]) == key
|
||||
}
|
||||
|
||||
// jsonObjectRawField scans the top-level keys of a JSON object looking for the
|
||||
// given field name, and returns its raw JSON value (e.g. `"full"` with quotes).
|
||||
// Returns nil if the field is not found.
|
||||
func jsonObjectRawField(data []byte, field string) json.Value {
|
||||
dec := json.NewDecoder(bytes.NewBuffer(data))
|
||||
if dec.PeekKind() != '{' {
|
||||
return nil
|
||||
}
|
||||
if _, err := dec.ReadToken(); err != nil {
|
||||
return nil
|
||||
}
|
||||
for dec.PeekKind() != '}' {
|
||||
name, err := dec.ReadValue()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
if jsonKeyCheck(name, field) {
|
||||
val, err := dec.ReadValue()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return val
|
||||
}
|
||||
if err := dec.SkipValue(); err != nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// jsonObjectHasKey scans the top-level keys of a JSON object looking for any of the
|
||||
// given keys. Returns the index of the first key found, or -1 if none match.
|
||||
// Bails early on first match without decoding any values.
|
||||
func jsonObjectHasKey(data []byte, keys ...string) int {
|
||||
dec := json.NewDecoder(bytes.NewBuffer(data))
|
||||
if dec.PeekKind() != '{' {
|
||||
return -1
|
||||
}
|
||||
if _, err := dec.ReadToken(); err != nil {
|
||||
return -1
|
||||
}
|
||||
for dec.PeekKind() != '}' {
|
||||
name, err := dec.ReadValue()
|
||||
if err != nil {
|
||||
return -1
|
||||
}
|
||||
for i, key := range keys {
|
||||
if jsonKeyCheck(name, key) {
|
||||
return i
|
||||
}
|
||||
}
|
||||
if err := dec.SkipValue(); err != nil {
|
||||
return -1
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// Inspired by https://www.youtube.com/watch?v=dab3I-HcTVk
|
||||
|
||||
type RequestInfo[Params, Resp any] struct {
|
||||
_ [0]Params
|
||||
_ [0]Resp
|
||||
Method Method
|
||||
}
|
||||
|
||||
func (info RequestInfo[Params, Resp]) UnmarshalResult(result any) (Resp, error) {
|
||||
raw, ok := result.(json.Value)
|
||||
if !ok {
|
||||
return *new(Resp), fmt.Errorf("expected json.Value, got %T", result)
|
||||
}
|
||||
|
||||
var r Resp
|
||||
if err := json.Unmarshal(raw, &r); err != nil {
|
||||
return *new(Resp), err
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func (info RequestInfo[Params, Resp]) NewRequestMessage(id *jsonrpc.ID, params Params) *RequestMessage {
|
||||
return &RequestMessage{
|
||||
ID: id,
|
||||
Method: info.Method,
|
||||
Params: params,
|
||||
}
|
||||
}
|
||||
|
||||
type NotificationInfo[Params any] struct {
|
||||
_ [0]Params
|
||||
Method Method
|
||||
}
|
||||
|
||||
func (info NotificationInfo[Params]) NewNotificationMessage(params Params) *RequestMessage {
|
||||
return &RequestMessage{
|
||||
Method: info.Method,
|
||||
Params: params,
|
||||
}
|
||||
}
|
||||
|
||||
// UnmarshalParams decodes the params of an inbound request or notification
|
||||
// message into the requested type. Inbound messages store their params as a
|
||||
// raw [json.Value] (see [Message.UnmarshalJSON]); decoding is deferred to the
|
||||
// point of dispatch so that param types for methods the server never handles
|
||||
// are not forced into the binary.
|
||||
//
|
||||
// A [NoParams] method must be given no params; every other method must be given
|
||||
// params as an object or array. A violation returns [ErrorCodeInvalidParams].
|
||||
func UnmarshalParams[T any](req *RequestMessage) (T, error) {
|
||||
var params T
|
||||
var raw json.Value
|
||||
if req.Params != nil {
|
||||
v, ok := req.Params.(json.Value)
|
||||
if !ok {
|
||||
return params, fmt.Errorf("%w: unexpected params type %T", ErrorCodeInvalidParams, req.Params)
|
||||
}
|
||||
raw = v
|
||||
}
|
||||
|
||||
// params is the zero value of T; this asserts on its type, i.e. whether the
|
||||
// method was declared with NoParams.
|
||||
if _, declaresNoParams := any(params).(NoParams); declaresNoParams {
|
||||
if len(raw) != 0 {
|
||||
return params, fmt.Errorf("%w: expected no params, got %s", ErrorCodeInvalidParams, raw)
|
||||
}
|
||||
return params, nil
|
||||
}
|
||||
|
||||
// The base protocol defines params as `array | object`; reject anything else
|
||||
// (absent, null, or a scalar).
|
||||
if k := raw.Kind(); k != '{' && k != '[' {
|
||||
return params, fmt.Errorf("%w: params must be an object or array", ErrorCodeInvalidParams)
|
||||
}
|
||||
if err := json.Unmarshal(raw, ¶ms); err != nil {
|
||||
return params, fmt.Errorf("%w: %w", ErrorCodeInvalidParams, err)
|
||||
}
|
||||
return params, nil
|
||||
}
|
||||
|
||||
type Null struct{}
|
||||
|
||||
func (Null) UnmarshalJSONFrom(dec *json.Decoder) error {
|
||||
data, err := dec.ReadValue()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if string(data) != "null" {
|
||||
return fmt.Errorf("expected null, got %s", data)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (Null) MarshalJSONTo(enc *json.Encoder) error {
|
||||
return enc.WriteToken(json.Null)
|
||||
}
|
||||
|
||||
type NoParams struct{}
|
||||
|
||||
func (NoParams) IsZero() bool { return true }
|
||||
|
||||
type clientCapabilitiesKey struct{}
|
||||
|
||||
func WithClientCapabilities(ctx context.Context, caps *ResolvedClientCapabilities) context.Context {
|
||||
return context.WithValue(ctx, clientCapabilitiesKey{}, caps)
|
||||
}
|
||||
|
||||
func GetClientCapabilities(ctx context.Context) *ResolvedClientCapabilities {
|
||||
if caps, _ := ctx.Value(clientCapabilitiesKey{}).(*ResolvedClientCapabilities); caps != nil {
|
||||
return caps
|
||||
}
|
||||
return &ResolvedClientCapabilities{}
|
||||
}
|
||||
|
||||
// PreferredMarkupKind returns the first (most preferred) markup kind from the given formats,
|
||||
// or MarkupKindPlainText if the slice is empty.
|
||||
func PreferredMarkupKind(formats []MarkupKind) MarkupKind {
|
||||
if len(formats) > 0 {
|
||||
return formats[0]
|
||||
}
|
||||
return MarkupKindPlainText
|
||||
}
|
||||
|
||||
const (
|
||||
CodeActionKindSourceRemoveUnusedImports CodeActionKind = "source.removeUnusedImports"
|
||||
CodeActionKindSourceSortImports CodeActionKind = "source.sortImports"
|
||||
)
|
||||
17262
tools/tsgo/internal/lsp/lsproto/lsp_generated.go
Normal file
17262
tools/tsgo/internal/lsp/lsproto/lsp_generated.go
Normal file
File diff suppressed because it is too large
Load Diff
1036
tools/tsgo/internal/lsp/lsproto/lsp_json_test.go
Normal file
1036
tools/tsgo/internal/lsp/lsproto/lsp_json_test.go
Normal file
File diff suppressed because it is too large
Load Diff
84
tools/tsgo/internal/lsp/lsproto/lsp_test.go
Normal file
84
tools/tsgo/internal/lsp/lsproto/lsp_test.go
Normal file
@@ -0,0 +1,84 @@
|
||||
package lsproto
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/json"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
func TestUnmarshalCompletionItem(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const message = `{
|
||||
"label": "pageXOffset",
|
||||
"insertTextFormat": 1,
|
||||
"textEdit": {
|
||||
"newText": "pageXOffset",
|
||||
"insert": {
|
||||
"start": {
|
||||
"line": 4,
|
||||
"character": 0
|
||||
},
|
||||
"end": {
|
||||
"line": 4,
|
||||
"character": 4
|
||||
}
|
||||
},
|
||||
"replace": {
|
||||
"start": {
|
||||
"line": 4,
|
||||
"character": 0
|
||||
},
|
||||
"end": {
|
||||
"line": 4,
|
||||
"character": 4
|
||||
}
|
||||
}
|
||||
},
|
||||
"kind": 6,
|
||||
"sortText": "15",
|
||||
"commitCharacters": [
|
||||
".",
|
||||
",",
|
||||
";"
|
||||
]
|
||||
}`
|
||||
|
||||
var result CompletionItem
|
||||
err := json.Unmarshal([]byte(message), &result)
|
||||
assert.NilError(t, err)
|
||||
|
||||
assert.DeepEqual(t, result, CompletionItem{
|
||||
Label: "pageXOffset",
|
||||
InsertTextFormat: new(InsertTextFormatPlainText),
|
||||
TextEdit: &TextEditOrInsertReplaceEdit{
|
||||
InsertReplaceEdit: &InsertReplaceEdit{
|
||||
NewText: "pageXOffset",
|
||||
Insert: Range{
|
||||
Start: Position{
|
||||
Line: 4,
|
||||
Character: 0,
|
||||
},
|
||||
End: Position{
|
||||
Line: 4,
|
||||
Character: 4,
|
||||
},
|
||||
},
|
||||
Replace: Range{
|
||||
Start: Position{
|
||||
Line: 4,
|
||||
Character: 0,
|
||||
},
|
||||
End: Position{
|
||||
Line: 4,
|
||||
Character: 4,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Kind: new(CompletionItemKindVariable),
|
||||
SortText: new("15"),
|
||||
CommitCharacters: new([]string{".", ",", ";"}),
|
||||
})
|
||||
}
|
||||
164
tools/tsgo/internal/lsp/lsproto/structcodec.go
Normal file
164
tools/tsgo/internal/lsp/lsproto/structcodec.go
Normal file
@@ -0,0 +1,164 @@
|
||||
package lsproto
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/json"
|
||||
)
|
||||
|
||||
// This file provides a single reflection-driven object decoder that replaces
|
||||
// the per-type UnmarshalJSONFrom methods the generator emits for plain
|
||||
// structs. It preserves the same strictness those methods enforce: the value
|
||||
// must be an object, all required fields must be present, and a JSON null is
|
||||
// rejected for nilable fields whose spec is not nullable. Required fields, and
|
||||
// the rare spec-nullable fields, are marked with an `lsp:"required"` /
|
||||
// `lsp:"nullable"` struct tag (the spec nullability that decides this is not
|
||||
// otherwise recoverable from the json tag); any other nilable field rejects
|
||||
// null by default. The per-type spec is resolved once via reflection and
|
||||
// cached, so the only per-call work is the object scan.
|
||||
|
||||
type structFieldSpec struct {
|
||||
index int
|
||||
requiredID int // bit position among required fields, or -1
|
||||
rejectNull bool
|
||||
}
|
||||
|
||||
type structSpec struct {
|
||||
byName map[string]structFieldSpec
|
||||
requiredNames []string
|
||||
requiredMask uint64
|
||||
}
|
||||
|
||||
var structSpecCache sync.Map // reflect.Type -> *structSpec
|
||||
|
||||
func specFor(t reflect.Type) *structSpec {
|
||||
if cached, ok := structSpecCache.Load(t); ok {
|
||||
return cached.(*structSpec)
|
||||
}
|
||||
spec := &structSpec{byName: make(map[string]structFieldSpec, t.NumField())}
|
||||
for i := range t.NumField() {
|
||||
f := t.Field(i)
|
||||
jsonName, _, _ := strings.Cut(f.Tag.Get("json"), ",")
|
||||
if jsonName == "" || jsonName == "-" {
|
||||
continue
|
||||
}
|
||||
fs := structFieldSpec{index: i, requiredID: -1}
|
||||
var nullable bool
|
||||
for marker := range strings.SplitSeq(f.Tag.Get("lsp"), ",") {
|
||||
switch marker {
|
||||
case "required":
|
||||
fs.requiredID = len(spec.requiredNames)
|
||||
spec.requiredMask |= 1 << fs.requiredID
|
||||
spec.requiredNames = append(spec.requiredNames, jsonName)
|
||||
case "nullable":
|
||||
nullable = true
|
||||
}
|
||||
}
|
||||
// A nilable field (pointer/slice/map) rejects an explicit JSON null
|
||||
// unless the spec marks it nullable.
|
||||
switch f.Type.Kind() {
|
||||
case reflect.Pointer, reflect.Slice, reflect.Map:
|
||||
fs.rejectNull = !nullable
|
||||
}
|
||||
spec.byName[jsonName] = fs
|
||||
}
|
||||
actual, _ := structSpecCache.LoadOrStore(t, spec)
|
||||
return actual.(*structSpec)
|
||||
}
|
||||
|
||||
// unmarshalStruct decodes a JSON object into the struct pointed to by v,
|
||||
// enforcing object-kind, required-field, and non-nullable-field strictness as
|
||||
// declared by lsp struct tags. Up to 64 required fields are supported.
|
||||
func unmarshalStruct(v any, dec *json.Decoder) error {
|
||||
rv := reflect.ValueOf(v).Elem()
|
||||
spec := specFor(rv.Type())
|
||||
|
||||
if k := dec.PeekKind(); k != '{' {
|
||||
return errNotObject(k)
|
||||
}
|
||||
if _, err := dec.ReadToken(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var seen uint64
|
||||
for dec.PeekKind() != '}' {
|
||||
name, err := dec.ReadValue()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// name includes surrounding quotes; m[string(b)] is a no-alloc lookup.
|
||||
fs, ok := spec.byName[string(name[1:len(name)-1])]
|
||||
if !ok {
|
||||
if err := dec.SkipValue(); err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
if fs.requiredID >= 0 {
|
||||
seen |= 1 << fs.requiredID
|
||||
}
|
||||
if fs.rejectNull && dec.PeekKind() == 'n' {
|
||||
return errNull(string(name[1 : len(name)-1]))
|
||||
}
|
||||
if err := json.UnmarshalDecode(dec, rv.Field(fs.index).Addr().Interface()); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if _, err := dec.ReadToken(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if missing := spec.requiredMask &^ seen; missing != 0 {
|
||||
var missingProps []string
|
||||
for id, n := range spec.requiredNames {
|
||||
if missing&(1<<id) != 0 {
|
||||
missingProps = append(missingProps, n)
|
||||
}
|
||||
}
|
||||
return errMissing(missingProps)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// marshalUnion encodes a union struct whose fields are all pointers, exactly
|
||||
// one of which is set. It writes the single non-nil field; if nullable, an
|
||||
// empty union marshals as null, otherwise an empty union is a programming
|
||||
// error. The name is only used for the panic message.
|
||||
func marshalUnion(v any, enc *json.Encoder, name string, nullable bool) error {
|
||||
rv := reflect.ValueOf(v).Elem()
|
||||
var set reflect.Value
|
||||
count := 0
|
||||
for _, f := range rv.Fields() {
|
||||
if !f.IsNil() {
|
||||
count++
|
||||
if !set.IsValid() {
|
||||
set = f
|
||||
}
|
||||
}
|
||||
}
|
||||
if nullable {
|
||||
assertAtMostOne("more than one element of "+name+" is set", count)
|
||||
if !set.IsValid() {
|
||||
return enc.WriteToken(json.Null)
|
||||
}
|
||||
} else {
|
||||
assertOnlyOne("exactly one element of "+name+" should be set", count)
|
||||
}
|
||||
return json.MarshalEncode(enc, set.Interface())
|
||||
}
|
||||
|
||||
// countNonNil returns the number of non-nil pointer/slice/map fields in the
|
||||
// struct pointed to by v. Used to assert externally-tagged unions have exactly
|
||||
// one arm set.
|
||||
func countNonNil(v any) int {
|
||||
rv := reflect.ValueOf(v).Elem()
|
||||
count := 0
|
||||
for _, f := range rv.Fields() {
|
||||
if !f.IsNil() {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
37
tools/tsgo/internal/lsp/lsproto/util.go
Normal file
37
tools/tsgo/internal/lsp/lsproto/util.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package lsproto
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
)
|
||||
|
||||
// Implements a cmp.Compare like function for two Position
|
||||
// ComparePositions(pos, other) == cmp.Compare(pos, other)
|
||||
func ComparePositions(pos, other Position) int {
|
||||
if lineComp := cmp.Compare(pos.Line, other.Line); lineComp != 0 {
|
||||
return lineComp
|
||||
}
|
||||
return cmp.Compare(pos.Character, other.Character)
|
||||
}
|
||||
|
||||
// Implements a cmp.Compare like function for two Range
|
||||
// CompareRanges(lsRange, other) == cmp.Compare(lsRange, other)
|
||||
//
|
||||
// Range.Start is compared before Range.End
|
||||
func CompareRanges(lsRange, other Range) int {
|
||||
if startComp := ComparePositions(lsRange.Start, other.Start); startComp != 0 {
|
||||
return startComp
|
||||
}
|
||||
return ComparePositions(lsRange.End, other.End)
|
||||
}
|
||||
|
||||
// AsString returns the plain text of a StringOrMarkupContent, reading the
|
||||
// MarkupContent value when the message is not a plain string.
|
||||
func (m StringOrMarkupContent) AsString() string {
|
||||
if m.String != nil {
|
||||
return *m.String
|
||||
}
|
||||
if m.MarkupContent != nil {
|
||||
return m.MarkupContent.Value
|
||||
}
|
||||
return ""
|
||||
}
|
||||
Reference in New Issue
Block a user