remove unused packages

This commit is contained in:
Egor Aristov 2025-10-15 17:27:26 +03:00
parent 2ba4cf4f1e
commit 5742ff6b75
Signed by: egor3f
GPG Key ID: 40482A264AAEC85F
12 changed files with 0 additions and 28979 deletions

View File

@ -1,2 +0,0 @@
metaModel.json
metaModel.schema.json

View File

@ -1,23 +0,0 @@
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");
const hash = "dadd73f7fc283b4d0adb602adadcf4be16ef3a7b";
const metaModelURL = `https://raw.githubusercontent.com/microsoft/vscode-languageserver-node/${hash}/protocol/metaModel.json`;
const metaModelSchemaURL = `https://raw.githubusercontent.com/microsoft/vscode-languageserver-node/${hash}/tools/src/metaModel.ts`;
const metaModelResponse = await fetch(metaModelURL);
let metaModel = await metaModelResponse.text();
metaModel = metaModel.replaceAll('"_InitializeParams"', '"InitializeParamsBase"');
fs.writeFileSync(metaModelPath, metaModel);
const metaModelSchemaResponse = await fetch(metaModelSchemaURL);
const metaModelSchema = await metaModelSchemaResponse.text();
fs.writeFileSync(metaModelSchemaPath, metaModelSchema);

View File

@ -1,913 +0,0 @@
#!/usr/bin/env node
import cp from "node:child_process";
import fs from "node:fs";
import path from "node:path";
import url from "node:url";
import which from "which";
import type {
MetaModel,
Notification,
OrType,
Property,
Request,
Structure,
Type,
} from "./metaModelSchema.mts";
const __filename = url.fileURLToPath(new URL(import.meta.url));
const __dirname = path.dirname(__filename);
const out = path.resolve(__dirname, "../lsp_generated.go");
const metaModelPath = path.resolve(__dirname, "metaModel.json");
if (!fs.existsSync(metaModelPath)) {
console.error("Meta model file not found; did you forget to run fetchModel.mjs?");
process.exit(1);
}
const model: MetaModel = JSON.parse(fs.readFileSync(metaModelPath, "utf-8"));
// Preprocess the model to inline extends/mixins contents
function preprocessModel() {
const structureMap = new Map<string, Structure>();
for (const structure of model.structures) {
structureMap.set(structure.name, structure);
}
function collectInheritedProperties(structure: Structure, visited = new Set<string>()): Property[] {
if (visited.has(structure.name)) {
return []; // Avoid circular dependencies
}
visited.add(structure.name);
const properties: Property[] = [];
const inheritanceTypes = [...(structure.extends || []), ...(structure.mixins || [])];
for (const type of inheritanceTypes) {
if (type.kind === "reference") {
const inheritedStructure = structureMap.get(type.name);
if (inheritedStructure) {
properties.push(
...collectInheritedProperties(inheritedStructure, new Set(visited)),
...inheritedStructure.properties,
);
}
}
}
return properties;
}
// Inline inheritance for each structure
for (const structure of model.structures) {
const inheritedProperties = collectInheritedProperties(structure);
// Merge properties with structure's own properties taking precedence
const propertyMap = new Map<string, Property>();
inheritedProperties.forEach(prop => propertyMap.set(prop.name, prop));
structure.properties.forEach(prop => propertyMap.set(prop.name, prop));
structure.properties = Array.from(propertyMap.values());
structure.extends = undefined;
structure.mixins = undefined;
}
}
// Preprocess the model before proceeding
preprocessModel();
interface GoType {
name: string;
needsPointer: boolean;
}
interface TypeInfo {
types: Map<string, GoType>;
literalTypes: Map<string, string>;
unionTypes: Map<string, { name: string; type: Type; containedNull: boolean; }[]>;
typeAliasMap: Map<string, Type>;
}
const typeInfo: TypeInfo = {
types: new Map(),
literalTypes: new Map(),
unionTypes: new Map(),
typeAliasMap: new Map(),
};
function titleCase(s: string) {
return s.charAt(0).toUpperCase() + s.slice(1);
}
function resolveType(type: Type): GoType {
switch (type.kind) {
case "base":
switch (type.name) {
case "integer":
return { name: "int32", needsPointer: false };
case "uinteger":
return { name: "uint32", needsPointer: false };
case "string":
return { name: "string", needsPointer: false };
case "boolean":
return { name: "bool", needsPointer: false };
case "URI":
return { name: "URI", needsPointer: false };
case "DocumentUri":
return { name: "DocumentUri", needsPointer: false };
case "decimal":
return { name: "float64", needsPointer: false };
case "null":
return { name: "any", needsPointer: false };
default:
throw new Error(`Unsupported base type: ${type.name}`);
}
case "reference":
const typeAliasOverride = typeAliasOverrides.get(type.name);
if (typeAliasOverride) {
return typeAliasOverride;
}
// Check if this is a type alias that resolves to a union type
const aliasedType = typeInfo.typeAliasMap.get(type.name);
if (aliasedType) {
return resolveType(aliasedType);
}
let refType = typeInfo.types.get(type.name);
if (!refType) {
refType = { name: type.name, needsPointer: true };
typeInfo.types.set(type.name, refType);
}
return refType;
case "array": {
const elementType = resolveType(type.element);
const arrayTypeName = elementType.needsPointer
? `[]*${elementType.name}`
: `[]${elementType.name}`;
return {
name: arrayTypeName,
needsPointer: false,
};
}
case "map": {
const keyType = resolveType(type.key);
const valueType = resolveType(type.value);
const valueTypeName = valueType.needsPointer ? `*${valueType.name}` : valueType.name;
return {
name: `map[${keyType.name}]${valueTypeName}`,
needsPointer: false,
};
}
case "tuple": {
if (
type.items.length === 2 &&
type.items[0].kind === "base" && type.items[0].name === "uinteger" &&
type.items[1].kind === "base" && type.items[1].name === "uinteger"
) {
return { name: "[2]uint32", needsPointer: false };
}
throw new Error("Unsupported tuple type: " + JSON.stringify(type));
}
case "stringLiteral": {
const typeName = `StringLiteral${titleCase(type.value)}`;
typeInfo.literalTypes.set(String(type.value), typeName);
return { name: typeName, needsPointer: false };
}
case "integerLiteral": {
const typeName = `IntegerLiteral${type.value}`;
typeInfo.literalTypes.set(String(type.value), typeName);
return { name: typeName, needsPointer: false };
}
case "booleanLiteral": {
const typeName = `BooleanLiteral${type.value ? "True" : "False"}`;
typeInfo.literalTypes.set(String(type.value), typeName);
return { name: typeName, needsPointer: false };
}
case "literal":
if (type.value.properties.length === 0) {
return { name: "struct{}", needsPointer: false };
}
throw new Error("Unexpected non-empty literal object: " + JSON.stringify(type.value));
case "or": {
return handleOrType(type);
}
default:
throw new Error(`Unsupported type kind: ${type.kind}`);
}
}
function flattenOrTypes(types: Type[]): Type[] {
const flattened = new Set<Type>();
for (const rawType of types) {
let type = rawType;
// Dereference reference types that point to OR types
if (rawType.kind === "reference") {
const aliasedType = typeInfo.typeAliasMap.get(rawType.name);
if (aliasedType && aliasedType.kind === "or") {
type = aliasedType;
}
}
if (type.kind === "or") {
// Recursively flatten OR types
for (const subType of flattenOrTypes(type.items)) {
flattened.add(subType);
}
}
else {
flattened.add(rawType);
}
}
return Array.from(flattened);
}
function handleOrType(orType: OrType): GoType {
// First, flatten any nested OR types
const types = flattenOrTypes(orType.items);
// Check for nullable types (OR with null)
const nullIndex = types.findIndex(item => item.kind === "base" && item.name === "null");
let containedNull = nullIndex !== -1;
// If it's nullable, remove the null type from the list
let nonNullTypes = types;
if (containedNull) {
nonNullTypes = types.filter((_, i) => i !== nullIndex);
}
// If no types remain after filtering null, this shouldn't happen
if (nonNullTypes.length === 0) {
throw new Error("Union type with only null is not supported: " + JSON.stringify(types));
}
// Even if only one type remains after filtering null, we still need to create a union type
// to preserve the nullable behavior (all fields nil = null)
let memberNames = nonNullTypes.map(type => {
if (type.kind === "reference") {
return type.name;
}
else if (type.kind === "base") {
return titleCase(type.name);
}
else if (
type.kind === "array" &&
(type.element.kind === "reference" || type.element.kind === "base")
) {
return `${titleCase(type.element.name)}s`;
}
else if (type.kind === "array") {
// Handle more complex array types
const elementType = resolveType(type.element);
return `${elementType.name}Array`;
}
else if (type.kind === "literal" && type.value.properties.length === 0) {
return "EmptyObject";
}
else if (type.kind === "tuple") {
return "Tuple";
}
else {
throw new Error(`Unsupported type kind in union: ${type.kind}`);
}
});
// Find longest common prefix of member names chunked by PascalCase
function findLongestCommonPrefix(names: string[]): string {
if (names.length === 0) return "";
if (names.length === 1) return "";
// Split each name into PascalCase chunks
function splitPascalCase(name: string): string[] {
const chunks: string[] = [];
let currentChunk = "";
for (let i = 0; i < name.length; i++) {
const char = name[i];
if (char >= "A" && char <= "Z" && currentChunk.length > 0) {
// Start of a new chunk
chunks.push(currentChunk);
currentChunk = char;
}
else {
currentChunk += char;
}
}
if (currentChunk.length > 0) {
chunks.push(currentChunk);
}
return chunks;
}
const allChunks = names.map(splitPascalCase);
const minChunkLength = Math.min(...allChunks.map(chunks => chunks.length));
// Find the longest common prefix of chunks
let commonChunks: string[] = [];
for (let i = 0; i < minChunkLength; i++) {
const chunk = allChunks[0][i];
if (allChunks.every(chunks => chunks[i] === chunk)) {
commonChunks.push(chunk);
}
else {
break;
}
}
return commonChunks.join("");
}
const commonPrefix = findLongestCommonPrefix(memberNames);
let unionTypeName = "";
if (commonPrefix.length > 0) {
const trimmedMemberNames = memberNames.map(name => name.slice(commonPrefix.length));
if (trimmedMemberNames.every(name => name)) {
unionTypeName = commonPrefix + trimmedMemberNames.join("Or");
memberNames = trimmedMemberNames;
}
else {
unionTypeName = memberNames.join("Or");
}
}
else {
unionTypeName = memberNames.join("Or");
}
if (containedNull) {
unionTypeName += "OrNull";
}
else {
containedNull = false;
}
const union = memberNames.map((name, i) => ({ name, type: nonNullTypes[i], containedNull }));
typeInfo.unionTypes.set(unionTypeName, union);
return {
name: unionTypeName,
needsPointer: false,
};
}
const typeAliasOverrides = new Map([
["LSPAny", { name: "any", needsPointer: false }],
["LSPArray", { name: "[]any", needsPointer: false }],
["LSPObject", { name: "map[string]any", needsPointer: false }],
]);
/**
* First pass: Resolve all type information
*/
function collectTypeDefinitions() {
// Process all enumerations first to make them available for struct fields
for (const enumeration of model.enumerations) {
typeInfo.types.set(enumeration.name, {
name: enumeration.name,
needsPointer: false,
});
}
const valueTypes = new Set([
"Position",
"Range",
"Location",
"Color",
"TextDocumentIdentifier",
"NotebookDocumentIdentifier",
"PreviousResultId",
"VersionedNotebookDocumentIdentifier",
"VersionedTextDocumentIdentifier",
"OptionalVersionedTextDocumentIdentifier",
]);
// Process all structures
for (const structure of model.structures) {
typeInfo.types.set(structure.name, {
name: structure.name,
needsPointer: !valueTypes.has(structure.name),
});
}
// Process all type aliases
for (const typeAlias of model.typeAliases) {
if (typeAliasOverrides.has(typeAlias.name)) {
continue;
}
// Store the alias mapping so we can resolve it later
typeInfo.typeAliasMap.set(typeAlias.name, typeAlias.type);
}
}
function formatDocumentation(s: string | undefined): string {
if (!s) return "";
let lines: string[] = [];
for (let line of s.split("\n")) {
line = line.trimEnd();
line = line.replace(/(\w ) +/g, "$1");
line = line.replace(/\{@link(?:code)?.*?([^} ]+)\}/g, "$1");
line = line.replace(/^@(since|proposed|deprecated)(.*)/, (_, tag, rest) => {
lines.push("");
return `${titleCase(tag)}${rest ? ":" + rest : "."}`;
});
lines.push(line);
}
// filter out contiguous empty lines
while (true) {
const toRemove = lines.findIndex((line, index) => {
if (line) return false;
if (index === 0) return true;
if (index === lines.length - 1) return true;
return !(lines[index - 1] && lines[index + 1]);
});
if (toRemove === -1) break;
lines.splice(toRemove, 1);
}
return lines.length > 0 ? "// " + lines.join("\n// ") + "\n" : "";
}
function methodNameIdentifier(name: string) {
return name.split("/").map(v => v === "$" ? "" : titleCase(v)).join("");
}
/**
* Generate the Go code
*/
function generateCode() {
const parts: string[] = [];
function write(s: string) {
parts.push(s);
}
function writeLine(s = "") {
parts.push(s + "\n");
}
// File header
writeLine("// Code generated by generate.mts; DO NOT EDIT.");
writeLine("");
writeLine("package lsproto");
writeLine("");
writeLine(`import (`);
writeLine(`\t"fmt"`);
writeLine("");
writeLine(`\t"github.com/go-json-experiment/json"`);
writeLine(`\t"github.com/go-json-experiment/json/jsontext"`);
writeLine(`)`);
writeLine("");
writeLine("// Meta model version " + model.metaData.version);
writeLine("");
// Generate structures
writeLine("// Structures\n");
for (const structure of model.structures) {
function generateStructFields(name: string, includeDocumentation: boolean) {
if (includeDocumentation) {
write(formatDocumentation(structure.documentation));
}
writeLine(`type ${name} struct {`);
// Properties are now inlined, no need to embed extends/mixins
for (const prop of structure.properties) {
if (includeDocumentation) {
write(formatDocumentation(prop.documentation));
}
const type = resolveType(prop.type);
const goType = prop.optional || type.needsPointer ? `*${type.name}` : type.name;
writeLine(`\t${titleCase(prop.name)} ${goType} \`json:"${prop.name}${prop.optional ? ",omitzero" : ""}"\``);
if (includeDocumentation) {
writeLine("");
}
}
writeLine("}");
writeLine("");
}
generateStructFields(structure.name, true);
writeLine("");
if (hasTextDocumentURI(structure)) {
// Generate TextDocumentURI method
writeLine(`func (s *${structure.name}) TextDocumentURI() DocumentUri {`);
writeLine(`\treturn s.TextDocument.Uri`);
writeLine(`}`);
writeLine("");
}
// Generate UnmarshalJSONFrom method for structure validation
const requiredProps = structure.properties?.filter(p => !p.optional) || [];
if (requiredProps.length > 0) {
writeLine(`\tvar _ json.UnmarshalerFrom = (*${structure.name})(nil)`);
writeLine("");
writeLine(`func (s *${structure.name}) UnmarshalJSONFrom(dec *jsontext.Decoder) error {`);
writeLine(`\tvar (`);
for (const prop of requiredProps) {
writeLine(`\t\tseen${titleCase(prop.name)} bool`);
}
writeLine(`\t)`);
writeLine("");
writeLine(`\tif k := dec.PeekKind(); k != '{' {`);
writeLine(`\t\treturn fmt.Errorf("expected object start, but encountered %v", k)`);
writeLine(`\t}`);
writeLine(`\tif _, err := dec.ReadToken(); err != nil {`);
writeLine(`\t\treturn err`);
writeLine(`\t}`);
writeLine("");
writeLine(`\tfor dec.PeekKind() != '}' {`);
writeLine("name, err := dec.ReadValue()");
writeLine(`\t\tif err != nil {`);
writeLine(`\t\t\treturn err`);
writeLine(`\t\t}`);
writeLine(`\t\tswitch string(name) {`);
for (const prop of structure.properties) {
writeLine(`\t\tcase \`"${prop.name}"\`:`);
if (!prop.optional) {
writeLine(`\t\t\tseen${titleCase(prop.name)} = true`);
}
writeLine(`\t\t\tif err := json.UnmarshalDecode(dec, &s.${titleCase(prop.name)}); err != nil {`);
writeLine(`\t\t\t\treturn err`);
writeLine(`\t\t\t}`);
}
writeLine(`\t\tdefault:`);
writeLine(`\t\t// Ignore unknown properties.`);
writeLine(`\t\t}`);
writeLine(`\t}`);
writeLine("");
writeLine(`\tif _, err := dec.ReadToken(); err != nil {`);
writeLine(`\t\treturn err`);
writeLine(`\t}`);
writeLine("");
for (const prop of requiredProps) {
writeLine(`\tif !seen${titleCase(prop.name)} {`);
writeLine(`\t\treturn fmt.Errorf("required property '${prop.name}' is missing")`);
writeLine(`\t}`);
}
writeLine("");
writeLine(`\treturn nil`);
writeLine(`}`);
writeLine("");
}
}
// Generate enumerations
writeLine("// Enumerations\n");
for (const enumeration of model.enumerations) {
write(formatDocumentation(enumeration.documentation));
let baseType;
switch (enumeration.type.name) {
case "string":
baseType = "string";
break;
case "integer":
baseType = "int32";
break;
case "uinteger":
baseType = "uint32";
break;
default:
throw new Error(`Unsupported enum type: ${enumeration.type.name}`);
}
writeLine(`type ${enumeration.name} ${baseType}`);
writeLine("");
// Get the pre-processed enum entries map that avoids duplicates
const enumValues = enumeration.values.map(value => ({
value: String(value.value),
identifier: `${enumeration.name}${value.name}`,
documentation: value.documentation,
deprecated: value.deprecated,
}));
writeLine("const (");
// Process entries with unique identifiers
for (const entry of enumValues) {
write(formatDocumentation(entry.documentation));
let valueLiteral;
// Handle string values
if (enumeration.type.name === "string") {
valueLiteral = `"${entry.value.replace(/^"|"$/g, "")}"`;
}
else {
valueLiteral = entry.value;
}
writeLine(`\t${entry.identifier} ${enumeration.name} = ${valueLiteral}`);
}
writeLine(")");
writeLine("");
}
const requestsAndNotifications: (Request | Notification)[] = [...model.requests, ...model.notifications];
// Generate unmarshalParams function
writeLine("func unmarshalParams(method Method, data []byte) (any, error) {");
writeLine("\tswitch method {");
// Requests and notifications
for (const request of requestsAndNotifications) {
const methodName = methodNameIdentifier(request.method);
if (!request.params) {
writeLine(`\tcase Method${methodName}:`);
writeLine(`\t\treturn unmarshalEmpty(data)`);
continue;
}
if (Array.isArray(request.params)) {
throw new Error("Unexpected array type for request params: " + JSON.stringify(request.params));
}
const resolvedType = resolveType(request.params);
writeLine(`\tcase Method${methodName}:`);
if (resolvedType.name === "any") {
writeLine(`\t\treturn unmarshalAny(data)`);
}
else {
writeLine(`\t\treturn unmarshalPtrTo[${resolvedType.name}](data)`);
}
}
writeLine("\tdefault:");
writeLine(`\t\treturn unmarshalAny(data)`);
writeLine("\t}");
writeLine("}");
writeLine("");
writeLine("// Methods");
writeLine("const (");
for (const request of requestsAndNotifications) {
write(formatDocumentation(request.documentation));
const methodName = methodNameIdentifier(request.method);
writeLine(`\tMethod${methodName} Method = "${request.method}"`);
}
writeLine(")");
writeLine("");
// Generate request response types
writeLine("// Request response types");
writeLine("");
for (const request of requestsAndNotifications) {
const methodName = methodNameIdentifier(request.method);
let responseTypeName: string | undefined;
if ("result" in request) {
if (request.typeName && request.typeName.endsWith("Request")) {
responseTypeName = request.typeName.replace(/Request$/, "Response");
}
else {
responseTypeName = `${methodName}Response`;
}
writeLine(`// Response type for \`${request.method}\``);
// Special case for response types that are explicitly base type "null"
if (request.result.kind === "base" && request.result.name === "null") {
writeLine(`type ${responseTypeName} = Null`);
}
else {
const resultType = resolveType(request.result);
const goType = resultType.needsPointer ? `*${resultType.name}` : resultType.name;
writeLine(`type ${responseTypeName} = ${goType}`);
}
writeLine("");
}
if (Array.isArray(request.params)) {
throw new Error("Unexpected request params for " + methodName + ": " + JSON.stringify(request.params));
}
const paramType = request.params ? resolveType(request.params) : undefined;
const paramGoType = paramType ? (paramType.needsPointer ? `*${paramType.name}` : paramType.name) : "any";
writeLine(`// Type mapping info for \`${request.method}\``);
if (responseTypeName) {
writeLine(`var ${methodName}Info = RequestInfo[${paramGoType}, ${responseTypeName}]{Method: Method${methodName}}`);
}
else {
writeLine(`var ${methodName}Info = NotificationInfo[${paramGoType}]{Method: Method${methodName}}`);
}
writeLine("");
}
// Generate union types
writeLine("// Union types\n");
for (const [name, members] of typeInfo.unionTypes.entries()) {
writeLine(`type ${name} struct {`);
const uniqueTypeFields = new Map(); // Maps type name -> field name
for (const member of members) {
const type = resolveType(member.type);
const memberType = type.name;
// If this type name already exists in our map, skip it
if (!uniqueTypeFields.has(memberType)) {
const fieldName = titleCase(member.name);
uniqueTypeFields.set(memberType, fieldName);
writeLine(`\t${fieldName} *${memberType}`);
}
}
writeLine(`}`);
writeLine("");
// Get the field names and types for marshal/unmarshal methods
const fieldEntries = Array.from(uniqueTypeFields.entries()).map(([typeName, fieldName]) => ({ fieldName, typeName }));
// Marshal method
writeLine(`var _ json.MarshalerTo = (*${name})(nil)`);
writeLine("");
writeLine(`func (o *${name}) MarshalJSONTo(enc *jsontext.Encoder) error {`);
// Determine if this union contained null (check if any member has containedNull = true)
const unionContainedNull = members.some(member => member.containedNull);
if (unionContainedNull) {
write(`\tassertAtMostOne("more than one element of ${name} is set", `);
}
else {
write(`\tassertOnlyOne("exactly one element of ${name} should be set", `);
}
// Create assertion to ensure at most one field is set at a time
// Write the assertion conditions
for (let i = 0; i < fieldEntries.length; i++) {
if (i > 0) write(", ");
write(`o.${fieldEntries[i].fieldName} != nil`);
}
writeLine(`)`);
writeLine("");
for (const entry of fieldEntries) {
writeLine(`\tif o.${entry.fieldName} != nil {`);
writeLine(`\t\treturn json.MarshalEncode(enc, o.${entry.fieldName})`);
writeLine(`\t}`);
}
// If all fields are nil, marshal as null (only for unions that can contain null)
if (unionContainedNull) {
writeLine(`\treturn enc.WriteToken(jsontext.Null)`);
}
else {
writeLine(`\tpanic("unreachable")`);
}
writeLine(`}`);
writeLine("");
// Unmarshal method
writeLine(`var _ json.UnmarshalerFrom = (*${name})(nil)`);
writeLine("");
writeLine(`func (o *${name}) UnmarshalJSONFrom(dec *jsontext.Decoder) error {`);
writeLine(`\t*o = ${name}{}`);
writeLine("");
writeLine("\tdata, err := dec.ReadValue()");
writeLine("\tif err != nil {");
writeLine("\t\treturn err");
writeLine("\t}");
if (unionContainedNull) {
writeLine(`\tif string(data) == "null" {`);
writeLine(`\t\treturn nil`);
writeLine(`\t}`);
writeLine("");
}
for (const entry of fieldEntries) {
writeLine(`\tvar v${entry.fieldName} ${entry.typeName}`);
writeLine(`\tif err := json.Unmarshal(data, &v${entry.fieldName}); err == nil {`);
writeLine(`\t\to.${entry.fieldName} = &v${entry.fieldName}`);
writeLine(`\t\treturn nil`);
writeLine(`\t}`);
}
// Match the error format from the original script
writeLine(`\treturn fmt.Errorf("invalid ${name}: %s", data)`);
writeLine(`}`);
writeLine("");
}
// Generate literal types
writeLine("// Literal types\n");
for (const [value, name] of typeInfo.literalTypes.entries()) {
const jsonValue = JSON.stringify(value);
writeLine(`// ${name} is a literal type for ${jsonValue}`);
writeLine(`type ${name} struct{}`);
writeLine("");
writeLine(`var _ json.MarshalerTo = ${name}{}`);
writeLine("");
writeLine(`func (o ${name}) MarshalJSONTo(enc *jsontext.Encoder) error {`);
writeLine(`\treturn enc.WriteValue(jsontext.Value(\`${jsonValue}\`))`);
writeLine(`}`);
writeLine("");
writeLine(`var _ json.UnmarshalerFrom = &${name}{}`);
writeLine("");
writeLine(`func (o *${name}) UnmarshalJSONFrom(dec *jsontext.Decoder) error {`);
writeLine(`\tv, err := dec.ReadValue();`);
writeLine(`\tif err != nil {`);
writeLine(`\t\treturn err`);
writeLine(`\t}`);
writeLine(`\tif string(v) != \`${jsonValue}\` {`);
writeLine(`\t\treturn fmt.Errorf("expected ${name} value %s, got %s", \`${jsonValue}\`, v)`);
writeLine(`\t}`);
writeLine(`\treturn nil`);
writeLine(`}`);
writeLine("");
}
return parts.join("");
}
function hasTextDocumentURI(structure: Structure) {
return structure.properties?.some(p =>
!p.optional &&
p.name === "textDocument" &&
p.type.kind === "reference" &&
p.type.name === "TextDocumentIdentifier"
);
}
/**
* Main function
*/
function main() {
try {
collectTypeDefinitions();
const generatedCode = generateCode();
fs.writeFileSync(out, generatedCode);
// Format with gofmt
const gofmt = which.sync("go");
cp.execFileSync(gofmt, ["tool", "mvdan.cc/gofumpt", "-lang=go1.25", "-w", out]);
console.log(`Successfully generated ${out}`);
}
catch (error) {
console.error("Error generating code:", error);
process.exit(1);
}
}
main();

View File

@ -1,609 +0,0 @@
/* --------------------------------------------------------------------------------------------
* 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;
};
/**
* 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;
};
/**
* 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;
};
/**
* 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[];
};

View File

@ -1,20 +0,0 @@
{
"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"
]
}

View File

@ -1,140 +0,0 @@
package lsproto
import (
"bufio"
"bytes"
"errors"
"fmt"
"io"
"strconv"
)
// https://microsoft.github.io/language-server-protocol/specifications/base/0.9/specification/
var (
ErrInvalidHeader = errors.New("lsp: invalid header")
ErrInvalidContentLength = errors.New("lsp: invalid content length")
ErrNoContentLength = errors.New("lsp: no content length")
)
type BaseReader struct {
r *bufio.Reader
}
func NewBaseReader(r io.Reader) *BaseReader {
return &BaseReader{
r: bufio.NewReader(r),
}
}
func (r *BaseReader) Read() ([]byte, error) {
var contentLength int64
for {
line, err := r.r.ReadBytes('\n')
if err != nil {
if errors.Is(err, io.EOF) {
return nil, io.EOF
}
return nil, fmt.Errorf("lsp: read header: %w", err)
}
if bytes.Equal(line, []byte("\r\n")) {
break
}
key, value, ok := bytes.Cut(line, []byte(":"))
if !ok {
return nil, fmt.Errorf("%w: %q", ErrInvalidHeader, line)
}
if bytes.Equal(key, []byte("Content-Length")) {
contentLength, err = strconv.ParseInt(string(bytes.TrimSpace(value)), 10, 64)
if err != nil {
return nil, fmt.Errorf("%w: parse error: %w", ErrInvalidContentLength, err)
}
if contentLength < 0 {
return nil, fmt.Errorf("%w: negative value %d", ErrInvalidContentLength, contentLength)
}
}
}
if contentLength <= 0 {
return nil, ErrNoContentLength
}
data := make([]byte, contentLength)
if _, err := io.ReadFull(r.r, data); err != nil {
return nil, fmt.Errorf("lsp: read content: %w", err)
}
return data, nil
}
type BaseWriter struct {
w *bufio.Writer
}
func NewBaseWriter(w io.Writer) *BaseWriter {
return &BaseWriter{
w: bufio.NewWriter(w),
}
}
func (w *BaseWriter) Write(data []byte) error {
if _, err := fmt.Fprintf(w.w, "Content-Length: %d\r\n\r\n", len(data)); err != nil {
return err
}
if _, err := w.w.Write(data); err != nil {
return err
}
return w.w.Flush()
}
type ErrorCode struct { //nolint:errname
Name string
Code int32
}
func (e *ErrorCode) Error() string {
return e.Name
}
var (
// Defined by JSON-RPC
ErrParseError = &ErrorCode{"ParseError", -32700}
ErrInvalidRequest = &ErrorCode{"InvalidRequest", -32600}
ErrMethodNotFound = &ErrorCode{"MethodNotFound", -32601}
ErrInvalidParams = &ErrorCode{"InvalidParams", -32602}
ErrInternalError = &ErrorCode{"InternalError", -32603}
// Error code indicating that a server received a notification or
// request before the server has received the `initialize` request.
ErrServerNotInitialized = &ErrorCode{"ServerNotInitialized", -32002}
ErrUnknownErrorCode = &ErrorCode{"UnknownErrorCode", -32001}
// A request failed but it was syntactically correct, e.g the
// method name was known and the parameters were valid. The error
// message should contain human readable information about why
// the request failed.
ErrRequestFailed = &ErrorCode{"RequestFailed", -32803}
// The server cancelled the request. This error code should
// only be used for requests that explicitly support being
// server cancellable.
ErrServerCancelled = &ErrorCode{"ServerCancelled", -32802}
// The server detected that the content of a document got
// modified outside normal conditions. A server should
// NOT send this error code if it detects a content change
// in it unprocessed messages. The result even computed
// on an older state might still be useful for the client.
//
// If a client decides that a result is not of any use anymore
// the client should cancel the request.
ErrContentModified = &ErrorCode{"ContentModified", -32801}
// The client has canceled a request and a server has detected
// the cancel.
ErrRequestCancelled = &ErrorCode{"RequestCancelled", -32800}
)

View File

@ -1,151 +0,0 @@
package lsproto_test
import (
"bytes"
"errors"
"testing"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/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: "lsp: no content length",
},
{
name: "early end",
input: []byte("oops"),
err: "EOF",
},
{
name: "negative length",
input: []byte("Content-Length: -1\r\n\r\n"),
err: "lsp: 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: "lsp: read content: unexpected EOF",
},
{
name: "missing content length",
input: []byte("Content-Length: \r\n\r\n{}"),
err: "lsp: invalid content length: parse error: strconv.ParseInt: parsing \"\": invalid syntax",
},
{
name: "invalid header",
input: []byte("Nope\r\n\r\n{}"),
err: "lsp: 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")
}

View File

@ -1,236 +0,0 @@
package lsproto
import (
"errors"
"fmt"
"strconv"
"github.com/go-json-experiment/json"
"github.com/go-json-experiment/json/jsontext"
)
type JSONRPCVersion struct{}
const jsonRPCVersion = `"2.0"`
func (JSONRPCVersion) MarshalJSON() ([]byte, error) {
return []byte(jsonRPCVersion), nil
}
var ErrInvalidJSONRPCVersion = errors.New("invalid JSON-RPC version")
func (*JSONRPCVersion) UnmarshalJSON(data []byte) error {
if string(data) != jsonRPCVersion {
return ErrInvalidJSONRPCVersion
}
return nil
}
type ID struct {
str string
int int32
}
func NewID(rawValue IntegerOrString) *ID {
if rawValue.String != nil {
return &ID{str: *rawValue.String}
}
return &ID{int: *rawValue.Integer}
}
func NewIDString(str string) *ID {
return &ID{str: str}
}
func (id *ID) String() string {
if id.str != "" {
return id.str
}
return strconv.Itoa(int(id.int))
}
func (id *ID) MarshalJSON() ([]byte, error) {
if id.str != "" {
return json.Marshal(id.str)
}
return json.Marshal(id.int)
}
func (id *ID) UnmarshalJSON(data []byte) error {
*id = ID{}
if len(data) > 0 && data[0] == '"' {
return json.Unmarshal(data, &id.str)
}
return json.Unmarshal(data, &id.int)
}
func (id *ID) TryInt() (int32, bool) {
if id == nil || id.str != "" {
return 0, false
}
return id.int, true
}
func (id *ID) MustInt() int32 {
if id.str != "" {
panic("ID is not an integer")
}
return id.int
}
type MessageKind int
const (
MessageKindNotification MessageKind = iota
MessageKindRequest
MessageKindResponse
)
type Message struct {
Kind 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 JSONRPCVersion `json:"jsonrpc"`
Method Method `json:"method"`
ID *ID `json:"id,omitzero"`
Params jsontext.Value `json:"params"`
Result any `json:"result,omitzero"`
Error *ResponseError `json:"error,omitzero"`
}
if err := json.Unmarshal(data, &raw); err != nil {
return fmt.Errorf("%w: %w", ErrInvalidRequest, err)
}
if raw.ID != nil && raw.Method == "" {
m.Kind = MessageKindResponse
m.msg = &ResponseMessage{
JSONRPC: raw.JSONRPC,
ID: raw.ID,
Result: raw.Result,
Error: raw.Error,
}
return nil
}
var params any
var err error
if len(raw.Params) > 0 {
params, err = unmarshalParams(raw.Method, raw.Params)
if err != nil {
return fmt.Errorf("%w: %w", ErrInvalidRequest, err)
}
}
if raw.ID == nil {
m.Kind = MessageKindNotification
} else {
m.Kind = MessageKindRequest
}
m.msg = &RequestMessage{
JSONRPC: raw.JSONRPC,
ID: raw.ID,
Method: raw.Method,
Params: params,
}
return nil
}
func (m *Message) MarshalJSON() ([]byte, error) {
return json.Marshal(m.msg)
}
func NewNotificationMessage(method Method, params any) *RequestMessage {
return &RequestMessage{
JSONRPC: JSONRPCVersion{},
Method: method,
Params: params,
}
}
type RequestMessage struct {
JSONRPC JSONRPCVersion `json:"jsonrpc"`
ID *ID `json:"id,omitzero"`
Method Method `json:"method"`
Params any `json:"params,omitzero"`
}
func NewRequestMessage(method Method, id *ID, params any) *RequestMessage {
return &RequestMessage{
ID: id,
Method: method,
Params: params,
}
}
func (r *RequestMessage) Message() *Message {
return &Message{
Kind: MessageKindRequest,
msg: r,
}
}
func (r *RequestMessage) UnmarshalJSON(data []byte) error {
var raw struct {
JSONRPC JSONRPCVersion `json:"jsonrpc"`
ID *ID `json:"id"`
Method Method `json:"method"`
Params jsontext.Value `json:"params"`
}
if err := json.Unmarshal(data, &raw); err != nil {
return fmt.Errorf("%w: %w", ErrInvalidRequest, err)
}
r.ID = raw.ID
r.Method = raw.Method
var err error
r.Params, err = unmarshalParams(raw.Method, raw.Params)
if err != nil {
return fmt.Errorf("%w: %w", ErrInvalidRequest, err)
}
return nil
}
type ResponseMessage struct {
JSONRPC JSONRPCVersion `json:"jsonrpc"`
ID *ID `json:"id,omitzero"`
Result any `json:"result,omitzero"`
Error *ResponseError `json:"error,omitzero"`
}
func (r *ResponseMessage) Message() *Message {
return &Message{
Kind: MessageKindResponse,
msg: r,
}
}
type ResponseError struct {
Code int32 `json:"code"`
Message string `json:"message"`
Data any `json:"data,omitzero"`
}
func (r *ResponseError) String() string {
if r == nil {
return ""
}
data, err := json.Marshal(r.Data)
if err != nil {
return fmt.Sprintf("[%d]: %s\n%v", r.Code, r.Message, data)
}
return fmt.Sprintf("[%d]: %s", r.Code, r.Message)
}

View File

@ -1,151 +0,0 @@
package lsproto
import (
"fmt"
"net/url"
"strings"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/core"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/tspath"
"github.com/go-json-experiment/json"
"github.com/go-json-experiment/json/jsontext"
)
type DocumentUri string // !!!
func (uri DocumentUri) FileName() string {
if strings.HasPrefix(string(uri), "file://") {
parsed := core.Must(url.Parse(string(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 URI string // !!!
type Method string
func unmarshalPtrTo[T any](data []byte) (*T, error) {
var v T
if err := json.Unmarshal(data, &v); err != nil {
return nil, fmt.Errorf("failed to unmarshal %T: %w", (*T)(nil), err)
}
return &v, nil
}
func unmarshalAny(data []byte) (any, error) {
var v any
if err := json.Unmarshal(data, &v); err != nil {
return nil, fmt.Errorf("failed to unmarshal any: %w", err)
}
return v, nil
}
func unmarshalEmpty(data []byte) (any, error) {
if len(data) != 0 {
return nil, fmt.Errorf("expected empty, got: %s", string(data))
}
return nil, nil
}
func assertOnlyOne(message string, values ...bool) {
count := 0
for _, v := range values {
if v {
count++
}
}
if count != 1 {
panic(message)
}
}
func assertAtMostOne(message string, values ...bool) {
count := 0
for _, v := range values {
if v {
count++
}
}
if count > 1 {
panic(message)
}
}
func ptrTo[T any](v T) *T {
return &v
}
type requiredProp bool
func (v *requiredProp) UnmarshalJSONFrom(dec *jsontext.Decoder) error {
*v = true
return dec.SkipValue()
}
// Inspired by https://www.youtube.com/watch?v=dab3I-HcTVk
type RequestInfo[Params, Resp any] struct {
_ [0]Params
_ [0]Resp
Method Method
}
type NotificationInfo[Params any] struct {
_ [0]Params
Method Method
}
type Null struct{}
func (Null) UnmarshalJSONFrom(dec *jsontext.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 *jsontext.Encoder) error {
return enc.WriteToken(jsontext.Null)
}

File diff suppressed because it is too large Load Diff

View File

@ -1,84 +0,0 @@
package lsproto
import (
"testing"
"github.com/go-json-experiment/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: ptrTo(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: ptrTo(CompletionItemKindVariable),
SortText: ptrTo("15"),
CommitCharacters: ptrTo([]string{".", ",", ";"}),
})
}

View File

@ -1,923 +0,0 @@
package lsp
import (
"context"
"errors"
"fmt"
"io"
"os"
"os/exec"
"os/signal"
"runtime/debug"
"slices"
"sync"
"sync/atomic"
"syscall"
"time"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/collections"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/core"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/ls"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/lsp/lsproto"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/project"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/project/ata"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/project/logging"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/tspath"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/vfs"
"github.com/go-json-experiment/json"
"golang.org/x/sync/errgroup"
"golang.org/x/text/language"
)
type ServerOptions struct {
In Reader
Out Writer
Err io.Writer
Cwd string
FS vfs.FS
DefaultLibraryPath string
TypingsLocation string
ParseCache *project.ParseCache
}
func NewServer(opts *ServerOptions) *Server {
if opts.Cwd == "" {
panic("Cwd is required")
}
return &Server{
r: opts.In,
w: opts.Out,
stderr: opts.Err,
logger: logging.NewLogger(opts.Err),
requestQueue: make(chan *lsproto.RequestMessage, 100),
outgoingQueue: make(chan *lsproto.Message, 100),
pendingClientRequests: make(map[lsproto.ID]pendingClientRequest),
pendingServerRequests: make(map[lsproto.ID]chan *lsproto.ResponseMessage),
cwd: opts.Cwd,
fs: opts.FS,
defaultLibraryPath: opts.DefaultLibraryPath,
typingsLocation: opts.TypingsLocation,
parseCache: opts.ParseCache,
}
}
var (
_ ata.NpmExecutor = (*Server)(nil)
_ project.Client = (*Server)(nil)
)
type pendingClientRequest struct {
req *lsproto.RequestMessage
cancel context.CancelFunc
}
type Reader interface {
Read() (*lsproto.Message, error)
}
type Writer interface {
Write(msg *lsproto.Message) error
}
type lspReader struct {
r *lsproto.BaseReader
}
type lspWriter struct {
w *lsproto.BaseWriter
}
func (r *lspReader) Read() (*lsproto.Message, error) {
data, err := r.r.Read()
if err != nil {
return nil, err
}
req := &lsproto.Message{}
if err := json.Unmarshal(data, req); err != nil {
return nil, fmt.Errorf("%w: %w", lsproto.ErrInvalidRequest, err)
}
return req, nil
}
func ToReader(r io.Reader) Reader {
return &lspReader{r: lsproto.NewBaseReader(r)}
}
func (w *lspWriter) Write(msg *lsproto.Message) error {
data, err := json.Marshal(msg)
if err != nil {
return fmt.Errorf("failed to marshal message: %w", err)
}
return w.w.Write(data)
}
func ToWriter(w io.Writer) Writer {
return &lspWriter{w: lsproto.NewBaseWriter(w)}
}
var (
_ Reader = (*lspReader)(nil)
_ Writer = (*lspWriter)(nil)
)
type Server struct {
r Reader
w Writer
stderr io.Writer
logger logging.Logger
clientSeq atomic.Int32
requestQueue chan *lsproto.RequestMessage
outgoingQueue chan *lsproto.Message
pendingClientRequests map[lsproto.ID]pendingClientRequest
pendingClientRequestsMu sync.Mutex
pendingServerRequests map[lsproto.ID]chan *lsproto.ResponseMessage
pendingServerRequestsMu sync.Mutex
cwd string
fs vfs.FS
defaultLibraryPath string
typingsLocation string
initializeParams *lsproto.InitializeParams
positionEncoding lsproto.PositionEncodingKind
locale language.Tag
watchEnabled bool
watcherID atomic.Uint32
watchers collections.SyncSet[project.WatcherID]
session *project.Session
// !!! temporary; remove when we have `handleDidChangeConfiguration`/implicit project config support
compilerOptionsForInferredProjects *core.CompilerOptions
// parseCache can be passed in so separate tests can share ASTs
parseCache *project.ParseCache
}
// WatchFiles implements project.Client.
func (s *Server) WatchFiles(ctx context.Context, id project.WatcherID, watchers []*lsproto.FileSystemWatcher) error {
_, err := s.sendRequest(ctx, lsproto.MethodClientRegisterCapability, &lsproto.RegistrationParams{
Registrations: []*lsproto.Registration{
{
Id: string(id),
Method: string(lsproto.MethodWorkspaceDidChangeWatchedFiles),
RegisterOptions: ptrTo(any(lsproto.DidChangeWatchedFilesRegistrationOptions{
Watchers: watchers,
})),
},
},
})
if err != nil {
return fmt.Errorf("failed to register file watcher: %w", err)
}
s.watchers.Add(id)
return nil
}
// UnwatchFiles implements project.Client.
func (s *Server) UnwatchFiles(ctx context.Context, id project.WatcherID) error {
if s.watchers.Has(id) {
_, err := s.sendRequest(ctx, lsproto.MethodClientUnregisterCapability, &lsproto.UnregistrationParams{
Unregisterations: []*lsproto.Unregistration{
{
Id: string(id),
Method: string(lsproto.MethodWorkspaceDidChangeWatchedFiles),
},
},
})
if err != nil {
return fmt.Errorf("failed to unregister file watcher: %w", err)
}
s.watchers.Delete(id)
return nil
}
return fmt.Errorf("no file watcher exists with ID %s", id)
}
// RefreshDiagnostics implements project.Client.
func (s *Server) RefreshDiagnostics(ctx context.Context) error {
if s.initializeParams.Capabilities == nil ||
s.initializeParams.Capabilities.Workspace == nil ||
s.initializeParams.Capabilities.Workspace.Diagnostics == nil ||
!ptrIsTrue(s.initializeParams.Capabilities.Workspace.Diagnostics.RefreshSupport) {
return nil
}
if _, err := s.sendRequest(ctx, lsproto.MethodWorkspaceDiagnosticRefresh, nil); err != nil {
return fmt.Errorf("failed to refresh diagnostics: %w", err)
}
return nil
}
func (s *Server) Run() error {
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
g, ctx := errgroup.WithContext(ctx)
g.Go(func() error { return s.dispatchLoop(ctx) })
g.Go(func() error { return s.writeLoop(ctx) })
// Don't run readLoop in the group, as it blocks on stdin read and cannot be cancelled.
readLoopErr := make(chan error, 1)
g.Go(func() error {
select {
case <-ctx.Done():
return ctx.Err()
case err := <-readLoopErr:
return err
}
})
go func() { readLoopErr <- s.readLoop(ctx) }()
if err := g.Wait(); err != nil && !errors.Is(err, io.EOF) && ctx.Err() != nil {
return err
}
return nil
}
func (s *Server) readLoop(ctx context.Context) error {
for {
if err := ctx.Err(); err != nil {
return err
}
msg, err := s.read()
if err != nil {
if errors.Is(err, lsproto.ErrInvalidRequest) {
s.sendError(nil, err)
continue
}
return err
}
if s.initializeParams == nil && msg.Kind == lsproto.MessageKindRequest {
req := msg.AsRequest()
if req.Method == lsproto.MethodInitialize {
resp, err := s.handleInitialize(ctx, req.Params.(*lsproto.InitializeParams), req)
if err != nil {
return err
}
s.sendResult(req.ID, resp)
} else {
s.sendError(req.ID, lsproto.ErrServerNotInitialized)
}
continue
}
if msg.Kind == lsproto.MessageKindResponse {
resp := msg.AsResponse()
s.pendingServerRequestsMu.Lock()
if respChan, ok := s.pendingServerRequests[*resp.ID]; ok {
respChan <- resp
close(respChan)
delete(s.pendingServerRequests, *resp.ID)
}
s.pendingServerRequestsMu.Unlock()
} else {
req := msg.AsRequest()
if req.Method == lsproto.MethodCancelRequest {
s.cancelRequest(req.Params.(*lsproto.CancelParams).Id)
} else {
s.requestQueue <- req
}
}
}
}
func (s *Server) cancelRequest(rawID lsproto.IntegerOrString) {
id := lsproto.NewID(rawID)
s.pendingClientRequestsMu.Lock()
defer s.pendingClientRequestsMu.Unlock()
if pendingReq, ok := s.pendingClientRequests[*id]; ok {
pendingReq.cancel()
delete(s.pendingClientRequests, *id)
}
}
func (s *Server) read() (*lsproto.Message, error) {
return s.r.Read()
}
func (s *Server) dispatchLoop(ctx context.Context) error {
ctx, lspExit := context.WithCancel(ctx)
defer lspExit()
for {
select {
case <-ctx.Done():
return ctx.Err()
case req := <-s.requestQueue:
requestCtx := core.WithLocale(ctx, s.locale)
if req.ID != nil {
var cancel context.CancelFunc
requestCtx, cancel = context.WithCancel(core.WithRequestID(requestCtx, req.ID.String()))
s.pendingClientRequestsMu.Lock()
s.pendingClientRequests[*req.ID] = pendingClientRequest{
req: req,
cancel: cancel,
}
s.pendingClientRequestsMu.Unlock()
}
handle := func() {
if err := s.handleRequestOrNotification(requestCtx, req); err != nil {
if errors.Is(err, context.Canceled) {
s.sendError(req.ID, lsproto.ErrRequestCancelled)
} else if errors.Is(err, io.EOF) {
lspExit()
} else {
s.sendError(req.ID, err)
}
}
if req.ID != nil {
s.pendingClientRequestsMu.Lock()
delete(s.pendingClientRequests, *req.ID)
s.pendingClientRequestsMu.Unlock()
}
}
if isBlockingMethod(req.Method) {
handle()
} else {
go handle()
}
}
}
}
func (s *Server) writeLoop(ctx context.Context) error {
for {
select {
case <-ctx.Done():
return ctx.Err()
case msg := <-s.outgoingQueue:
if err := s.w.Write(msg); err != nil {
return fmt.Errorf("failed to write message: %w", err)
}
}
}
}
func (s *Server) sendRequest(ctx context.Context, method lsproto.Method, params any) (any, error) {
id := lsproto.NewIDString(fmt.Sprintf("ts%d", s.clientSeq.Add(1)))
req := lsproto.NewRequestMessage(method, id, params)
responseChan := make(chan *lsproto.ResponseMessage, 1)
s.pendingServerRequestsMu.Lock()
s.pendingServerRequests[*id] = responseChan
s.pendingServerRequestsMu.Unlock()
s.outgoingQueue <- req.Message()
select {
case <-ctx.Done():
s.pendingServerRequestsMu.Lock()
defer s.pendingServerRequestsMu.Unlock()
if respChan, ok := s.pendingServerRequests[*id]; ok {
close(respChan)
delete(s.pendingServerRequests, *id)
}
return nil, ctx.Err()
case resp := <-responseChan:
if resp.Error != nil {
return nil, fmt.Errorf("request failed: %s", resp.Error.String())
}
return resp.Result, nil
}
}
func (s *Server) sendResult(id *lsproto.ID, result any) {
s.sendResponse(&lsproto.ResponseMessage{
ID: id,
Result: result,
})
}
func (s *Server) sendError(id *lsproto.ID, err error) {
code := lsproto.ErrInternalError.Code
if errCode := (*lsproto.ErrorCode)(nil); errors.As(err, &errCode) {
code = errCode.Code
}
// TODO(jakebailey): error data
s.sendResponse(&lsproto.ResponseMessage{
ID: id,
Error: &lsproto.ResponseError{
Code: code,
Message: err.Error(),
},
})
}
func (s *Server) sendResponse(resp *lsproto.ResponseMessage) {
s.outgoingQueue <- resp.Message()
}
func (s *Server) handleRequestOrNotification(ctx context.Context, req *lsproto.RequestMessage) error {
if handler := handlers()[req.Method]; handler != nil {
return handler(s, ctx, req)
}
s.Log("unknown method", req.Method)
if req.ID != nil {
s.sendError(req.ID, lsproto.ErrInvalidRequest)
}
return nil
}
type handlerMap map[lsproto.Method]func(*Server, context.Context, *lsproto.RequestMessage) error
var handlers = sync.OnceValue(func() handlerMap {
handlers := make(handlerMap)
registerRequestHandler(handlers, lsproto.InitializeInfo, (*Server).handleInitialize)
registerNotificationHandler(handlers, lsproto.InitializedInfo, (*Server).handleInitialized)
registerRequestHandler(handlers, lsproto.ShutdownInfo, (*Server).handleShutdown)
registerNotificationHandler(handlers, lsproto.ExitInfo, (*Server).handleExit)
registerNotificationHandler(handlers, lsproto.TextDocumentDidOpenInfo, (*Server).handleDidOpen)
registerNotificationHandler(handlers, lsproto.TextDocumentDidChangeInfo, (*Server).handleDidChange)
registerNotificationHandler(handlers, lsproto.TextDocumentDidSaveInfo, (*Server).handleDidSave)
registerNotificationHandler(handlers, lsproto.TextDocumentDidCloseInfo, (*Server).handleDidClose)
registerNotificationHandler(handlers, lsproto.WorkspaceDidChangeWatchedFilesInfo, (*Server).handleDidChangeWatchedFiles)
registerNotificationHandler(handlers, lsproto.SetTraceInfo, (*Server).handleSetTrace)
registerLanguageServiceDocumentRequestHandler(handlers, lsproto.TextDocumentDiagnosticInfo, (*Server).handleDocumentDiagnostic)
registerLanguageServiceDocumentRequestHandler(handlers, lsproto.TextDocumentHoverInfo, (*Server).handleHover)
registerLanguageServiceDocumentRequestHandler(handlers, lsproto.TextDocumentDefinitionInfo, (*Server).handleDefinition)
registerLanguageServiceDocumentRequestHandler(handlers, lsproto.TextDocumentTypeDefinitionInfo, (*Server).handleTypeDefinition)
registerLanguageServiceDocumentRequestHandler(handlers, lsproto.TextDocumentCompletionInfo, (*Server).handleCompletion)
registerLanguageServiceDocumentRequestHandler(handlers, lsproto.TextDocumentReferencesInfo, (*Server).handleReferences)
registerLanguageServiceDocumentRequestHandler(handlers, lsproto.TextDocumentImplementationInfo, (*Server).handleImplementations)
registerLanguageServiceDocumentRequestHandler(handlers, lsproto.TextDocumentSignatureHelpInfo, (*Server).handleSignatureHelp)
registerLanguageServiceDocumentRequestHandler(handlers, lsproto.TextDocumentFormattingInfo, (*Server).handleDocumentFormat)
registerLanguageServiceDocumentRequestHandler(handlers, lsproto.TextDocumentRangeFormattingInfo, (*Server).handleDocumentRangeFormat)
registerLanguageServiceDocumentRequestHandler(handlers, lsproto.TextDocumentOnTypeFormattingInfo, (*Server).handleDocumentOnTypeFormat)
registerLanguageServiceDocumentRequestHandler(handlers, lsproto.TextDocumentDocumentSymbolInfo, (*Server).handleDocumentSymbol)
registerLanguageServiceDocumentRequestHandler(handlers, lsproto.TextDocumentRenameInfo, (*Server).handleRename)
registerLanguageServiceDocumentRequestHandler(handlers, lsproto.TextDocumentDocumentHighlightInfo, (*Server).handleDocumentHighlight)
registerRequestHandler(handlers, lsproto.WorkspaceSymbolInfo, (*Server).handleWorkspaceSymbol)
registerRequestHandler(handlers, lsproto.CompletionItemResolveInfo, (*Server).handleCompletionItemResolve)
return handlers
})
func registerNotificationHandler[Req any](handlers handlerMap, info lsproto.NotificationInfo[Req], fn func(*Server, context.Context, Req) error) {
handlers[info.Method] = func(s *Server, ctx context.Context, req *lsproto.RequestMessage) error {
if s.session == nil && req.Method != lsproto.MethodInitialized {
return lsproto.ErrServerNotInitialized
}
var params Req
// Ignore empty params; all generated params are either pointers or any.
if req.Params != nil {
params = req.Params.(Req)
}
if err := fn(s, ctx, params); err != nil {
return err
}
return ctx.Err()
}
}
func registerRequestHandler[Req, Resp any](
handlers handlerMap,
info lsproto.RequestInfo[Req, Resp],
fn func(*Server, context.Context, Req, *lsproto.RequestMessage) (Resp, error),
) {
handlers[info.Method] = func(s *Server, ctx context.Context, req *lsproto.RequestMessage) error {
if s.session == nil && req.Method != lsproto.MethodInitialize {
return lsproto.ErrServerNotInitialized
}
var params Req
// Ignore empty params.
if req.Params != nil {
params = req.Params.(Req)
}
resp, err := fn(s, ctx, params, req)
if err != nil {
return err
}
if ctx.Err() != nil {
return ctx.Err()
}
s.sendResult(req.ID, resp)
return nil
}
}
func registerLanguageServiceDocumentRequestHandler[Req lsproto.HasTextDocumentURI, Resp any](handlers handlerMap, info lsproto.RequestInfo[Req, Resp], fn func(*Server, context.Context, *ls.LanguageService, Req) (Resp, error)) {
handlers[info.Method] = func(s *Server, ctx context.Context, req *lsproto.RequestMessage) error {
var params Req
// Ignore empty params.
if req.Params != nil {
params = req.Params.(Req)
}
ls, err := s.session.GetLanguageService(ctx, params.TextDocumentURI())
if err != nil {
return err
}
defer s.recover(req)
resp, err := fn(s, ctx, ls, params)
if err != nil {
return err
}
if ctx.Err() != nil {
return ctx.Err()
}
s.sendResult(req.ID, resp)
return nil
}
}
func (s *Server) recover(req *lsproto.RequestMessage) {
if r := recover(); r != nil {
stack := debug.Stack()
s.Log("panic handling request", req.Method, r, string(stack))
if req.ID != nil {
s.sendError(req.ID, fmt.Errorf("%w: panic handling request %s: %v", lsproto.ErrInternalError, req.Method, r))
} else {
s.Log("unhandled panic in notification", req.Method, r)
}
}
}
func (s *Server) handleInitialize(ctx context.Context, params *lsproto.InitializeParams, _ *lsproto.RequestMessage) (lsproto.InitializeResponse, error) {
if s.initializeParams != nil {
return nil, lsproto.ErrInvalidRequest
}
s.initializeParams = params
s.positionEncoding = lsproto.PositionEncodingKindUTF16
if genCapabilities := s.initializeParams.Capabilities.General; genCapabilities != nil && genCapabilities.PositionEncodings != nil {
if slices.Contains(*genCapabilities.PositionEncodings, lsproto.PositionEncodingKindUTF8) {
s.positionEncoding = lsproto.PositionEncodingKindUTF8
}
}
if s.initializeParams.Locale != nil {
locale, err := language.Parse(*s.initializeParams.Locale)
if err != nil {
return nil, err
}
s.locale = locale
}
if s.initializeParams.Trace != nil && *s.initializeParams.Trace == "verbose" {
s.logger.SetVerbose(true)
}
response := &lsproto.InitializeResult{
ServerInfo: &lsproto.ServerInfo{
Name: "typescript-go",
Version: ptrTo(core.Version()),
},
Capabilities: &lsproto.ServerCapabilities{
PositionEncoding: ptrTo(s.positionEncoding),
TextDocumentSync: &lsproto.TextDocumentSyncOptionsOrKind{
Options: &lsproto.TextDocumentSyncOptions{
OpenClose: ptrTo(true),
Change: ptrTo(lsproto.TextDocumentSyncKindIncremental),
Save: &lsproto.BooleanOrSaveOptions{
Boolean: ptrTo(true),
},
},
},
HoverProvider: &lsproto.BooleanOrHoverOptions{
Boolean: ptrTo(true),
},
DefinitionProvider: &lsproto.BooleanOrDefinitionOptions{
Boolean: ptrTo(true),
},
TypeDefinitionProvider: &lsproto.BooleanOrTypeDefinitionOptionsOrTypeDefinitionRegistrationOptions{
Boolean: ptrTo(true),
},
ReferencesProvider: &lsproto.BooleanOrReferenceOptions{
Boolean: ptrTo(true),
},
ImplementationProvider: &lsproto.BooleanOrImplementationOptionsOrImplementationRegistrationOptions{
Boolean: ptrTo(true),
},
DiagnosticProvider: &lsproto.DiagnosticOptionsOrRegistrationOptions{
Options: &lsproto.DiagnosticOptions{
InterFileDependencies: true,
},
},
CompletionProvider: &lsproto.CompletionOptions{
TriggerCharacters: &ls.TriggerCharacters,
ResolveProvider: ptrTo(true),
// !!! other options
},
SignatureHelpProvider: &lsproto.SignatureHelpOptions{
TriggerCharacters: &[]string{"(", ","},
},
DocumentFormattingProvider: &lsproto.BooleanOrDocumentFormattingOptions{
Boolean: ptrTo(true),
},
DocumentRangeFormattingProvider: &lsproto.BooleanOrDocumentRangeFormattingOptions{
Boolean: ptrTo(true),
},
DocumentOnTypeFormattingProvider: &lsproto.DocumentOnTypeFormattingOptions{
FirstTriggerCharacter: "{",
MoreTriggerCharacter: &[]string{"}", ";", "\n"},
},
WorkspaceSymbolProvider: &lsproto.BooleanOrWorkspaceSymbolOptions{
Boolean: ptrTo(true),
},
DocumentSymbolProvider: &lsproto.BooleanOrDocumentSymbolOptions{
Boolean: ptrTo(true),
},
RenameProvider: &lsproto.BooleanOrRenameOptions{
Boolean: ptrTo(true),
},
DocumentHighlightProvider: &lsproto.BooleanOrDocumentHighlightOptions{
Boolean: ptrTo(true),
},
},
}
return response, nil
}
func (s *Server) handleInitialized(ctx context.Context, params *lsproto.InitializedParams) error {
if shouldEnableWatch(s.initializeParams) {
s.watchEnabled = true
}
cwd := s.cwd
if s.initializeParams.Capabilities != nil &&
s.initializeParams.Capabilities.Workspace != nil &&
s.initializeParams.Capabilities.Workspace.WorkspaceFolders != nil &&
ptrIsTrue(s.initializeParams.Capabilities.Workspace.WorkspaceFolders) &&
s.initializeParams.WorkspaceFolders != nil &&
s.initializeParams.WorkspaceFolders.WorkspaceFolders != nil &&
len(*s.initializeParams.WorkspaceFolders.WorkspaceFolders) == 1 {
cwd = lsproto.DocumentUri((*s.initializeParams.WorkspaceFolders.WorkspaceFolders)[0].Uri).FileName()
} else if s.initializeParams.RootUri.DocumentUri != nil {
cwd = s.initializeParams.RootUri.DocumentUri.FileName()
} else if s.initializeParams.RootPath != nil && s.initializeParams.RootPath.String != nil {
cwd = *s.initializeParams.RootPath.String
}
if !tspath.PathIsAbsolute(cwd) {
cwd = s.cwd
}
s.session = project.NewSession(&project.SessionInit{
Options: &project.SessionOptions{
CurrentDirectory: cwd,
DefaultLibraryPath: s.defaultLibraryPath,
TypingsLocation: s.typingsLocation,
PositionEncoding: s.positionEncoding,
WatchEnabled: s.watchEnabled,
LoggingEnabled: true,
DebounceDelay: 500 * time.Millisecond,
},
FS: s.fs,
Logger: s.logger,
Client: s,
NpmExecutor: s,
ParseCache: s.parseCache,
})
// !!! temporary; remove when we have `handleDidChangeConfiguration`/implicit project config support
if s.compilerOptionsForInferredProjects != nil {
s.session.DidChangeCompilerOptionsForInferredProjects(ctx, s.compilerOptionsForInferredProjects)
}
return nil
}
func (s *Server) handleShutdown(ctx context.Context, params any, _ *lsproto.RequestMessage) (lsproto.ShutdownResponse, error) {
s.session.Close()
return lsproto.ShutdownResponse{}, nil
}
func (s *Server) handleExit(ctx context.Context, params any) error {
return io.EOF
}
func (s *Server) handleDidOpen(ctx context.Context, params *lsproto.DidOpenTextDocumentParams) error {
s.session.DidOpenFile(ctx, params.TextDocument.Uri, params.TextDocument.Version, params.TextDocument.Text, params.TextDocument.LanguageId)
return nil
}
func (s *Server) handleDidChange(ctx context.Context, params *lsproto.DidChangeTextDocumentParams) error {
s.session.DidChangeFile(ctx, params.TextDocument.Uri, params.TextDocument.Version, params.ContentChanges)
return nil
}
func (s *Server) handleDidSave(ctx context.Context, params *lsproto.DidSaveTextDocumentParams) error {
s.session.DidSaveFile(ctx, params.TextDocument.Uri)
return nil
}
func (s *Server) handleDidClose(ctx context.Context, params *lsproto.DidCloseTextDocumentParams) error {
s.session.DidCloseFile(ctx, params.TextDocument.Uri)
return nil
}
func (s *Server) handleDidChangeWatchedFiles(ctx context.Context, params *lsproto.DidChangeWatchedFilesParams) error {
s.session.DidChangeWatchedFiles(ctx, params.Changes)
return nil
}
func (s *Server) handleSetTrace(ctx context.Context, params *lsproto.SetTraceParams) error {
switch params.Value {
case "verbose":
s.logger.SetVerbose(true)
case "messages":
s.logger.SetVerbose(false)
case "off":
// !!! logging cannot be completely turned off for now
s.logger.SetVerbose(false)
default:
return fmt.Errorf("unknown trace value: %s", params.Value)
}
return nil
}
func (s *Server) handleDocumentDiagnostic(ctx context.Context, ls *ls.LanguageService, params *lsproto.DocumentDiagnosticParams) (lsproto.DocumentDiagnosticResponse, error) {
return ls.ProvideDiagnostics(ctx, params.TextDocument.Uri)
}
func (s *Server) handleHover(ctx context.Context, ls *ls.LanguageService, params *lsproto.HoverParams) (lsproto.HoverResponse, error) {
return ls.ProvideHover(ctx, params.TextDocument.Uri, params.Position)
}
func (s *Server) handleSignatureHelp(ctx context.Context, languageService *ls.LanguageService, params *lsproto.SignatureHelpParams) (lsproto.SignatureHelpResponse, error) {
return languageService.ProvideSignatureHelp(
ctx,
params.TextDocument.Uri,
params.Position,
params.Context,
s.initializeParams.Capabilities.TextDocument.SignatureHelp,
&ls.UserPreferences{},
)
}
func (s *Server) handleDefinition(ctx context.Context, ls *ls.LanguageService, params *lsproto.DefinitionParams) (lsproto.DefinitionResponse, error) {
return ls.ProvideDefinition(ctx, params.TextDocument.Uri, params.Position)
}
func (s *Server) handleTypeDefinition(ctx context.Context, ls *ls.LanguageService, params *lsproto.TypeDefinitionParams) (lsproto.TypeDefinitionResponse, error) {
return ls.ProvideTypeDefinition(ctx, params.TextDocument.Uri, params.Position)
}
func (s *Server) handleReferences(ctx context.Context, ls *ls.LanguageService, params *lsproto.ReferenceParams) (lsproto.ReferencesResponse, error) {
// findAllReferences
return ls.ProvideReferences(ctx, params)
}
func (s *Server) handleImplementations(ctx context.Context, ls *ls.LanguageService, params *lsproto.ImplementationParams) (lsproto.ImplementationResponse, error) {
// goToImplementation
return ls.ProvideImplementations(ctx, params)
}
func (s *Server) handleCompletion(ctx context.Context, languageService *ls.LanguageService, params *lsproto.CompletionParams) (lsproto.CompletionResponse, error) {
// !!! get user preferences
return languageService.ProvideCompletion(
ctx,
params.TextDocument.Uri,
params.Position,
params.Context,
getCompletionClientCapabilities(s.initializeParams),
&ls.UserPreferences{
IncludeCompletionsForModuleExports: core.TSTrue,
IncludeCompletionsForImportStatements: core.TSTrue,
})
}
func (s *Server) handleCompletionItemResolve(ctx context.Context, params *lsproto.CompletionItem, reqMsg *lsproto.RequestMessage) (lsproto.CompletionResolveResponse, error) {
data, err := ls.GetCompletionItemData(params)
if err != nil {
return nil, err
}
languageService, err := s.session.GetLanguageService(ctx, ls.FileNameToDocumentURI(data.FileName))
if err != nil {
return nil, err
}
defer s.recover(reqMsg)
return languageService.ResolveCompletionItem(
ctx,
params,
data,
getCompletionClientCapabilities(s.initializeParams),
&ls.UserPreferences{
IncludeCompletionsForModuleExports: core.TSTrue,
IncludeCompletionsForImportStatements: core.TSTrue,
},
)
}
func (s *Server) handleDocumentFormat(ctx context.Context, ls *ls.LanguageService, params *lsproto.DocumentFormattingParams) (lsproto.DocumentFormattingResponse, error) {
return ls.ProvideFormatDocument(
ctx,
params.TextDocument.Uri,
params.Options,
)
}
func (s *Server) handleDocumentRangeFormat(ctx context.Context, ls *ls.LanguageService, params *lsproto.DocumentRangeFormattingParams) (lsproto.DocumentRangeFormattingResponse, error) {
return ls.ProvideFormatDocumentRange(
ctx,
params.TextDocument.Uri,
params.Options,
params.Range,
)
}
func (s *Server) handleDocumentOnTypeFormat(ctx context.Context, ls *ls.LanguageService, params *lsproto.DocumentOnTypeFormattingParams) (lsproto.DocumentOnTypeFormattingResponse, error) {
return ls.ProvideFormatDocumentOnType(
ctx,
params.TextDocument.Uri,
params.Options,
params.Position,
params.Ch,
)
}
func (s *Server) handleWorkspaceSymbol(ctx context.Context, params *lsproto.WorkspaceSymbolParams, reqMsg *lsproto.RequestMessage) (lsproto.WorkspaceSymbolResponse, error) {
snapshot, release := s.session.Snapshot()
defer release()
defer s.recover(reqMsg)
programs := core.Map(snapshot.ProjectCollection.Projects(), (*project.Project).GetProgram)
return ls.ProvideWorkspaceSymbols(ctx, programs, snapshot.Converters(), params.Query)
}
func (s *Server) handleDocumentSymbol(ctx context.Context, ls *ls.LanguageService, params *lsproto.DocumentSymbolParams) (lsproto.DocumentSymbolResponse, error) {
return ls.ProvideDocumentSymbols(ctx, params.TextDocument.Uri)
}
func (s *Server) handleRename(ctx context.Context, ls *ls.LanguageService, params *lsproto.RenameParams) (lsproto.RenameResponse, error) {
return ls.ProvideRename(ctx, params)
}
func (s *Server) handleDocumentHighlight(ctx context.Context, ls *ls.LanguageService, params *lsproto.DocumentHighlightParams) (lsproto.DocumentHighlightResponse, error) {
return ls.ProvideDocumentHighlights(ctx, params.TextDocument.Uri, params.Position)
}
func (s *Server) Log(msg ...any) {
fmt.Fprintln(s.stderr, msg...)
}
// !!! temporary; remove when we have `handleDidChangeConfiguration`/implicit project config support
func (s *Server) SetCompilerOptionsForInferredProjects(ctx context.Context, options *core.CompilerOptions) {
s.compilerOptionsForInferredProjects = options
if s.session != nil {
s.session.DidChangeCompilerOptionsForInferredProjects(ctx, options)
}
}
// NpmInstall implements ata.NpmExecutor
func (s *Server) NpmInstall(cwd string, args []string) ([]byte, error) {
cmd := exec.Command("npm", args...)
cmd.Dir = cwd
return cmd.Output()
}
func isBlockingMethod(method lsproto.Method) bool {
switch method {
case lsproto.MethodInitialize,
lsproto.MethodInitialized,
lsproto.MethodTextDocumentDidOpen,
lsproto.MethodTextDocumentDidChange,
lsproto.MethodTextDocumentDidSave,
lsproto.MethodTextDocumentDidClose,
lsproto.MethodWorkspaceDidChangeWatchedFiles:
return true
}
return false
}
func ptrTo[T any](v T) *T {
return &v
}
func ptrIsTrue(v *bool) bool {
if v == nil {
return false
}
return *v
}
func shouldEnableWatch(params *lsproto.InitializeParams) bool {
if params == nil || params.Capabilities == nil || params.Capabilities.Workspace == nil {
return false
}
return params.Capabilities.Workspace.DidChangeWatchedFiles != nil &&
ptrIsTrue(params.Capabilities.Workspace.DidChangeWatchedFiles.DynamicRegistration)
}
func getCompletionClientCapabilities(params *lsproto.InitializeParams) *lsproto.CompletionClientCapabilities {
if params == nil || params.Capabilities == nil || params.Capabilities.TextDocument == nil {
return nil
}
return params.Capabilities.TextDocument.Completion
}