diff --git a/internal/lsp/protocol/typescript/README.md b/internal/lsp/protocol/typescript/README.md index 92f52acb..4084c4f6 100644 --- a/internal/lsp/protocol/typescript/README.md +++ b/internal/lsp/protocol/typescript/README.md @@ -3,14 +3,15 @@ ## Setup 1. Make sure `node` is installed. - * As explained at the [node site](https://nodejs.org Node) - * You may need `node install @types/node` for the node runtime types + As explained at the [node site]( Node) + you may need `node install @types/node` for the node runtime types 2. Install the typescript compiler, with `node install typescript`. 3. Make sure `tsc` and `node` are in your execution path. 4. Get the typescript code for the jsonrpc protocol with `git clone vscode-lanuageserver-node.git` ## Usage +To generated the protocol types (x/tools/internal/lsp/protocol/tsprotocol.go) ```tsc go.ts && node go.js [-d dir] [-o out.go]``` and for simple checking @@ -23,8 +24,11 @@ It defaults to `$(HOME)`. `-o out.go` says where the generated go code goes. It defaults to `/tmp/tsprotocol.go`. -(The output file cannot yet be used to build `gopls`. That will be fixed in a future CL.) +To generate the client and server boilerplate (tsclient.go and tsserver.go) +```tsc requests.ts && node requests.js [-d dir] && gofmt -w tsclient.go tsserver.go``` + +-d dir is the same as above. The output files are written into the current directory. ## Note -`go.ts` uses the Typescript compiler's API, which is [introduced](https://github.com/Microsoft/TypeScript/wiki/Architectural-Overview API) in their wiki. \ No newline at end of file +`go.ts` uses the Typescript compiler's API, which is [introduced]( API) in their wiki. \ No newline at end of file diff --git a/internal/lsp/protocol/typescript/go.ts b/internal/lsp/protocol/typescript/go.ts index c78728f8..09f4fb28 100644 --- a/internal/lsp/protocol/typescript/go.ts +++ b/internal/lsp/protocol/typescript/go.ts @@ -1,7 +1,9 @@ import * as fs from 'fs'; import * as ts from 'typescript'; - +// 1. Everything that returns a Go thing should have unusable?: boolean +// 2. Remember what gets exported, and don't print the others (so _ can stay) +// 3. Merge all intersection types, and probably Heritage types too interface Const { typeName: string // repeated in each const goType: string @@ -15,8 +17,9 @@ let seenConstTypes = new Map(); interface Struct { me: ts.Node name: string - embeds: string[] - fields?: Field[] + embeds?: string[] + fields?: Field[]; + extends?: string[] } let Structs: Struct[] = []; @@ -46,9 +49,10 @@ function seenAdd(x: string) { } let dir = process.env['HOME']; +const srcDir = '/vscode-languageserver-node' let fnames = [ - `/vscode-languageserver-node/protocol/src/protocol.ts`, - `/vscode-languageserver-node/types/src/main.ts` + `${srcDir}/protocol/src/protocol.ts`, `${srcDir}/types/src/main.ts`, + `${srcDir}/jsonrpc/src/main.ts` ]; let outFname = '/tmp/tsprotocol.go'; let fda: number, fdb: number, fde: number; // file descriptors @@ -68,6 +72,9 @@ function prgo(s: string) { return (fs.writeSync(fde, s)) } +// struct names that don't need to go in the output +let dontEmit = new Map(); + function generate(files: string[], options: ts.CompilerOptions): void { let program = ts.createProgram(files, options); program.getTypeChecker(); // used for side-effects @@ -93,11 +100,11 @@ function generate(files: string[], options: ts.CompilerOptions): void { return; function genTypes(node: ts.Node) { - // Ignore top-level items with no output + // Ignore top-level items that produce no output if (ts.isExpressionStatement(node) || ts.isFunctionDeclaration(node) || - ts.isImportDeclaration(node) || ts.isVariableStatement(node) || - ts.isExportDeclaration(node) || - node.kind == ts.SyntaxKind.EndOfFileToken) { + ts.isImportDeclaration(node) || ts.isVariableStatement(node) || + ts.isExportDeclaration(node) || + node.kind == ts.SyntaxKind.EndOfFileToken) { return; } if (ts.isInterfaceDeclaration(node)) { @@ -112,18 +119,17 @@ function generate(files: string[], options: ts.CompilerOptions): void { } else if (ts.isClassDeclaration(node)) { doClassDeclaration(node) } else { - throw new Error(`unexpected ${ts.SyntaxKind[node.kind]} ${loc(node)}`) + throw new Error(`unexpected ${strKind(node)} ${loc(node)}`) } } function doClassDeclaration(node: ts.ClassDeclaration) { - let id: ts.Identifier + let id: ts.Identifier = node.name; let props = new Array() let extend: ts.HeritageClause; let bad = false node.forEachChild((n: ts.Node) => { if (ts.isIdentifier(n)) { - id = n; return } if (ts.isPropertyDeclaration(n)) { @@ -134,20 +140,24 @@ function generate(files: string[], options: ts.CompilerOptions): void { return } if (n.kind == ts.SyntaxKind.Constructor || ts.isMethodDeclaration(n) || - ts.isGetAccessor(n) || ts.isTypeParameterDeclaration(n)) { + ts.isGetAccessor(n) || ts.isSetAccessor(n) || + ts.isTypeParameterDeclaration(n)) { bad = true; return } if (ts.isHeritageClause(n)) { - extend = n; return } + if (n.kind == ts.SyntaxKind.AbstractKeyword) { + bad = true; // we think all of these are useless, but are unsure + return; + } throw new Error(`doClass ${loc(n)} ${kinds(n)}`) }) if (bad) { // the class is not useful for Go. return - } // might we want the PropertyDecls? (don't think so) + } let fields: Field[] = []; for (const pr of props) { fields.push(fromPropDecl(pr)) @@ -155,37 +165,26 @@ function generate(files: string[], options: ts.CompilerOptions): void { let ans = { me: node, name: toGoName(getText(id)), - embeds: heritageStrs(extend), + extends: heritageStrs(extend), fields: fields }; Structs.push(ans) } function fromPropDecl(node: ts.PropertyDeclaration): Field { - let id: ts.Identifier; - let opt = false - let typ: ts.Node - node.forEachChild((n: ts.Node) => { - if (ts.isIdentifier(n)) { - id = n; - return - } - if (n.kind == ts.SyntaxKind.QuestionToken) { - opt = true; - return - } - if (typ != undefined) - throw new Error(`fromPropDecl too long ${loc(node)}`) - typ = n - }) - let goType = computeType(typ).goType + let id: ts.Identifier = (ts.isIdentifier(node.name) && node.name); + let opt = node.questionToken != undefined; + let typ: ts.Node = node.type; + const computed = computeType(typ); + let goType = computed.goType let ans = { me: node, id: id, goName: toGoName(getText(id)), optional: opt, goType: goType, - json: `\`json:"${id.text}${opt ? ',omitempty' : ''}"\`` + json: `\`json:"${id.text}${opt ? ',omitempty' : ''}"\``, + substruct: computed.fields }; return ans } @@ -199,16 +198,16 @@ function generate(files: string[], options: ts.CompilerOptions): void { // find the Identifier from children // process the PropertySignature children // the members might have generic info, but so do the children - let id: ts.Identifier; + let id: ts.Identifier = node.name let extend: ts.HeritageClause let generid: ts.Identifier let properties = new Array() let index: ts.IndexSignatureDeclaration // generate some sort of map + let bad = false; // maybe we don't care about this one at all node.forEachChild((n: ts.Node) => { if (n.kind == ts.SyntaxKind.ExportKeyword || ts.isMethodSignature(n)) { // ignore } else if (ts.isIdentifier(n)) { - id = n; } else if (ts.isHeritageClause(n)) { extend = n; } else if (ts.isTypeParameterDeclaration(n)) { @@ -221,10 +220,13 @@ function generate(files: string[], options: ts.CompilerOptions): void { throw new Error(`${loc(n)} multiple index expressions`) } index = n + } else if (n.kind == ts.SyntaxKind.CallSignature) { + bad = true; } else { - throw new Error(`${loc(n)} doInterface ${ts.SyntaxKind[n.kind]} `) + throw new Error(`${loc(n)} doInterface ${strKind(n)} `) } }) + if (bad) return; let fields: Field[] = []; for (const p of properties) { fields.push(genProp(p, generid)) @@ -235,7 +237,7 @@ function generate(files: string[], options: ts.CompilerOptions): void { const ans = { me: node, name: toGoName(getText(id)), - embeds: heritageStrs(extend), + extends: heritageStrs(extend), fields: fields }; @@ -287,7 +289,7 @@ function generate(files: string[], options: ts.CompilerOptions): void { } }) let goName = toGoName(id.text) - let { goType, gostuff, optional, fields } = computeType(thing) + let {goType, gostuff, optional, fields} = computeType(thing) // Generics if (gen && gen.text == goType) goType = 'interface{}'; opt = opt || optional; @@ -301,12 +303,13 @@ function generate(files: string[], options: ts.CompilerOptions): void { substruct: fields, json: `\`json:"${id.text}${opt ? ',omitempty' : ''}"\`` }; - // Rather than checking that goName is a const type, just do + // These are not structs, so '*' would be wrong. switch (goType) { case 'CompletionItemKind': case 'TextDocumentSyncKind': case 'CodeActionKind': - case 'InsertTextFormat': // float64 + case 'FailureHandlingKind': // string + case 'InsertTextFormat': // float64 case 'DiagnosticSeverity': ans.optional = false } @@ -315,18 +318,8 @@ function generate(files: string[], options: ts.CompilerOptions): void { function doModuleDeclaration(node: ts.ModuleDeclaration) { // Export Identifier ModuleBlock - let id: ts.Identifier; - let mb: ts.ModuleBlock; - node.forEachChild((n: ts.Node) => { - if ((ts.isIdentifier(n) && (id = n)) || - (ts.isModuleBlock(n) && mb === undefined && (mb = n)) || - (n.kind == ts.SyntaxKind.ExportKeyword)) { - return; - } - throw new Error(`doModuleDecl ${loc(n)} ${ts.SyntaxKind[n.kind]}`) - }) + let id: ts.Identifier = (ts.isIdentifier(node.name) && node.name); // Don't want FunctionDeclarations - // mb has VariableStatement and useless TypeAliasDeclaration // some of the VariableStatement are consts, and want their comments // and each VariableStatement is Export, VariableDeclarationList // and each VariableDeclarationList is a single VariableDeclaration @@ -352,15 +345,13 @@ function generate(files: string[], options: ts.CompilerOptions): void { function buildConst(tname: string, node: ts.VariableDeclaration): Const { // node is Identifier, optional-goo, (FirstLiteralToken|StringLiteral) - let id: ts.Identifier + let id: ts.Identifier = (ts.isIdentifier(node.name) && node.name); let str: string let first: string node.forEachChild((n: ts.Node) => { - if (ts.isIdentifier(n)) { - id = n - } else if (ts.isStringLiteral(n)) { + if (ts.isStringLiteral(n)) { str = getText(n) - } else if (n.kind == ts.SyntaxKind.FirstLiteralToken) { + } else if (n.kind == ts.SyntaxKind.NumericLiteral) { first = getText(n) } }) @@ -393,17 +384,9 @@ function generate(files: string[], options: ts.CompilerOptions): void { function doEnumDecl(node: ts.EnumDeclaration) { // Generates Consts. Identifier EnumMember+ // EnumMember: Identifier StringLiteral - let id: ts.Identifier - let mems: ts.EnumMember[] = [] - node.forEachChild((n: ts.Node) => { - if (ts.isIdentifier(n)) { - id = n // check for uniqueness? - } else if (ts.isEnumMember(n)) { - mems.push(n) - } else if (n.kind != ts.SyntaxKind.ExportKeyword) { - throw new Error(`doEnumDecl ${ts.SyntaxKind[n.kind]} ${loc(n)}`) - } - }) + let id: ts.Identifier = node.name + let mems = node.members + let theType = 'string'; for (const m of mems) { let name: string let value: string @@ -412,13 +395,16 @@ function generate(files: string[], options: ts.CompilerOptions): void { name = getText(n) } else if (ts.isStringLiteral(n)) { value = getText(n).replace(/'/g, '"') + } else if (ts.isNumericLiteral(n)) { + value = getText(n); + theType = 'float64'; } else { - throw new Error(`in doEnumDecl ${ts.SyntaxKind[n.kind]} ${loc(n)}`) + throw new Error(`in doEnumDecl ${strKind(n)} ${loc(n)}`) } }) let ans = { typeName: getText(id), - goType: 'string', + goType: theType, me: m, name: name, value: value @@ -430,31 +416,20 @@ function generate(files: string[], options: ts.CompilerOptions): void { // top-level TypeAlias function doTypeAlias(node: ts.TypeAliasDeclaration) { // these are all Export Identifier alias - let id: ts.Identifier; - let alias: ts.Node; - let genid: ts.TypeParameterDeclaration // , but we don't care - node.forEachChild((n: ts.Node) => { - if ((ts.isIdentifier(n) && (id = n)) || - (n.kind == ts.SyntaxKind.ExportKeyword) || - ts.isTypeParameterDeclaration(n) && (genid = n) || - (alias === undefined && (alias = n))) { - return - } - throw new Error(`doTypeAlias ${loc(n)} ${ts.SyntaxKind[n.kind]}`) - }) + let id: ts.Identifier = node.name; + let alias: ts.Node = node.type; let ans = { me: node, id: id, goName: toGoName(getText(id)), - goType: '?', + goType: '?', // filled in later in this function stuff: '' }; - if (id.text.indexOf('--') != -1) { - return - } // don't care if (ts.isUnionTypeNode(alias)) { ans.goType = weirdUnionType(alias) - if (ans.goType == undefined) { // these are redundant + if (id.text == 'DocumentFilter') + if (ans.goType == undefined) { + // these are mostly redundant; maybe sort them out later return } Types.push(ans) @@ -464,11 +439,14 @@ function generate(files: string[], options: ts.CompilerOptions): void { let embeds: string[] = [] alias.forEachChild((n: ts.Node) => { if (ts.isTypeReferenceNode(n)) { - embeds.push(toGoName(computeType(n).goType)) + const s = toGoName(computeType(n).goType) + embeds.push(s) + // It's here just for embedding, and not used independently + dontEmit.set(s, true); } else - throw new Error(`expected TypeRef ${ts.SyntaxKind[n.kind]} ${loc(n)}`) + throw new Error(`expected TypeRef ${strKind(n)} ${loc(n)}`) }) - let ans = { me: node, name: toGoName(getText(id)), embeds: embeds }; + let ans = {me: node, name: toGoName(getText(id)), embeds: embeds}; Structs.push(ans) return } @@ -480,6 +458,9 @@ function generate(files: string[], options: ts.CompilerOptions): void { if (ts.isLiteralTypeNode(alias)) { return // type A = 1, so nope } + if (ts.isTypeLiteralNode(alias)) { + return; // type A = {...} + } if (ts.isTypeReferenceNode(alias)) { ans.goType = computeType(alias).goType if (ans.goType.match(/und/) != null) throw new Error('396') @@ -491,21 +472,36 @@ function generate(files: string[], options: ts.CompilerOptions): void { Types.push(ans); return } - throw new Error(`in doTypeAlias ${loc(node)} ${kinds(node)} ${ - ts.SyntaxKind[alias.kind]}\n`) + throw new Error( + `in doTypeAlias ${loc(alias)} ${kinds(node)}: ${strKind(alias)}\n`) } - // extract the one useful but weird case () + // string, or number, or DocumentFilter function weirdUnionType(node: ts.UnionTypeNode): string { - let bad = false + let bad = false; + let aNumber = false; + let aString = false; let tl: ts.TypeLiteralNode[] = [] node.forEachChild((n: ts.Node) => { if (ts.isTypeLiteralNode(n)) { - tl.push(n) - } else - bad = true + tl.push(n); + return; + } + if (ts.isLiteralTypeNode(n)) { + n.literal.kind == ts.SyntaxKind.NumericLiteral ? aNumber = true : + aString = true; + return; + } + bad = true }) - if (bad) return // none of these are useful (so far) + if (bad) return; // none of these are useful (so far) + if (aNumber) { + if (aString) + throw new Error( + `weirdUnionType is both number and string ${loc(node)}`); + return 'float64'; + } + if (aString) return 'string'; let x = computeType(tl[0]) x.fields[0].json = x.fields[0].json.replace(/"`/, ',omitempty"`') let out: string[] = []; @@ -517,24 +513,25 @@ function generate(files: string[], options: ts.CompilerOptions): void { return ans } - function computeType(node: ts.Node): { goType: string, gostuff?: string, optional?: boolean, fields?: Field[] } { + function computeType(node: ts.Node): + {goType: string, gostuff?: string, optional?: boolean, fields?: Field[]} { switch (node.kind) { case ts.SyntaxKind.AnyKeyword: case ts.SyntaxKind.ObjectKeyword: - return { goType: 'interface{}' }; + return {goType: 'interface{}'}; case ts.SyntaxKind.BooleanKeyword: - return { goType: 'bool' }; + return {goType: 'bool'}; case ts.SyntaxKind.NumberKeyword: - return { goType: 'float64' }; + return {goType: 'float64'}; case ts.SyntaxKind.StringKeyword: - return { goType: 'string' }; + return {goType: 'string'}; case ts.SyntaxKind.NullKeyword: case ts.SyntaxKind.UndefinedKeyword: - return { goType: 'nil' }; + return {goType: 'nil'}; } if (ts.isArrayTypeNode(node)) { - let { goType, gostuff, optional } = computeType(node.elementType) - return ({ goType: '[]' + goType, gostuff: gostuff, optional: optional }) + let {goType, gostuff, optional} = computeType(node.elementType) + return ({goType: '[]' + goType, gostuff: gostuff, optional: optional}) } else if (ts.isTypeReferenceNode(node)) { // typeArguments?: NodeArray;typeName: EntityName; // typeArguments won't show up in the generated Go @@ -543,10 +540,10 @@ function generate(files: string[], options: ts.CompilerOptions): void { if (ts.isQualifiedName(tn)) { throw new Error(`qualified name at ${loc(node)}`); } else if (ts.isIdentifier(tn)) { - return { goType: tn.text }; + return {goType: tn.text}; } else { - throw new Error(`expected identifier got ${ - ts.SyntaxKind[node.typeName.kind]} at ${loc(tn)}`) + throw new Error( + `expected identifier got ${strKind(node.typeName)} at ${loc(tn)}`) } } else if (ts.isLiteralTypeNode(node)) { // string|float64 (are there other possibilities?) @@ -555,7 +552,7 @@ function generate(files: string[], options: ts.CompilerOptions): void { if (txt.charAt(0) == '\'') { typ = 'string' } - return { goType: typ, gostuff: getText(node) }; + return {goType: typ, gostuff: getText(node)}; } else if (ts.isTypeLiteralNode(node)) { let x: Field[] = []; let indexCnt = 0 @@ -568,32 +565,34 @@ function generate(files: string[], options: ts.CompilerOptions): void { x.push(fromIndexSignature(n)) return } - throw new Error( - `${loc(n)} gotype ${ts.SyntaxKind[n.kind]}, not expected`) + throw new Error(`${loc(n)} gotype ${strKind(n)}, not expected`) }); if (indexCnt > 0) { if (indexCnt != 1 || x.length != 1) throw new Error(`undexpected Index ${loc(x[0].me)}`) - // instead of {map...} just the map - return ({ goType: x[0].goType, gostuff: x[0].gostuff }) + // instead of {map...} just the map + return ({goType: x[0].goType, gostuff: x[0].gostuff}) } - return ({ goType: 'embedded!', fields: x }) + return ({goType: 'embedded!', fields: x}) } else if (ts.isUnionTypeNode(node)) { - let x = new Array<{ goType: string, gostuff?: string, optiona?: boolean }>() - node.forEachChild((n: ts.Node) => { x.push(computeType(n)) }) + let x = new Array<{goType: string, gostuff?: string, optiona?: boolean}>() + node.forEachChild((n: ts.Node) => {x.push(computeType(n))}) if (x.length == 2 && x[1].goType == 'nil') { return x[0] // make it optional somehow? TODO } if (x[0].goType == 'bool') { // take it - return ({ goType: 'bool', gostuff: getText(node) }) + if (x[1].goType == 'RenameOptions') { + return ({goType: 'RenameOptions', gostuff: getText(node)}) + } + return ({goType: 'bool', gostuff: getText(node)}) } // these are special cases from looking at the source let gostuff = getText(node); if (x[0].goType == `"off"` || x[0].goType == 'string') { - return ({ goType: 'string', gostuff: gostuff }) + return ({goType: 'string', gostuff: gostuff}) } if (x[0].goType == 'TextDocumentSyncOptions') { - return ({ goType: 'interface{}', gostuff: gostuff }) + return ({goType: 'interface{}', gostuff: gostuff}) } if (x[0].goType == 'float64' && x[1].goType == 'string') { return { @@ -605,6 +604,11 @@ function generate(files: string[], options: ts.CompilerOptions): void { goType: 'MarkupContent', gostuff: gostuff } } + if (x[0].goType == 'RequestMessage' && x[1].goType == 'ResponseMessage') { + return { + goType: 'interface{}', gostuff: gostuff + } + } // Fail loudly console.log(`UnionType ${loc(node)}`) for (const v of x) { @@ -613,17 +617,22 @@ function generate(files: string[], options: ts.CompilerOptions): void { throw new Error('in UnionType, weird') } else if (ts.isParenthesizedTypeNode(node)) { // check that this is (TextDocumentEdit | CreateFile | RenameFile | - // DeleteFile) + // DeleteFile) TODO(pjw) return { goType: 'TextDocumentEdit', gostuff: getText(node) } } else if (ts.isTupleTypeNode(node)) { - // string | [number, number] + // string | [number, number]. TODO(pjw): check it really is return { goType: 'string', gostuff: getText(node) } + } else if (ts.isFunctionTypeNode(node)) { + // we don't want these members; mark them + return { + goType: 'bad', gostuff: getText(node) + } } - throw new Error(`unknown ${ts.SyntaxKind[node.kind]} at ${loc(node)}`) + throw new Error(`computeType unknown ${strKind(node)} at ${loc(node)}`) } function fromIndexSignature(node: ts.IndexSignatureDeclaration): Field { @@ -633,11 +642,11 @@ function generate(files: string[], options: ts.CompilerOptions): void { if (ts.isParameter(n)) { parm = n } else if ( - ts.isArrayTypeNode(n) || n.kind == ts.SyntaxKind.AnyKeyword || - ts.isUnionTypeNode(n)) { + ts.isArrayTypeNode(n) || n.kind == ts.SyntaxKind.AnyKeyword || + ts.isUnionTypeNode(n)) { at = n } else - throw new Error(`fromIndexSig ${ts.SyntaxKind[n.kind]} ${loc(n)}`) + throw new Error(`fromIndexSig ${strKind(n)} ${loc(n)}`) }) let goType = computeType(at).goType let id: ts.Identifier @@ -645,15 +654,14 @@ function generate(files: string[], options: ts.CompilerOptions): void { if (ts.isIdentifier(n)) { id = n } else if (n.kind != ts.SyntaxKind.StringKeyword) { - throw new Error( - `fromIndexSig expected string, ${ts.SyntaxKind[n.kind]} ${loc(n)}`) + throw new Error(`fromIndexSig expected string, ${strKind(n)} ${loc(n)}`) } }) goType = `map[string]${goType}` return { me: node, goName: toGoName(id.text), id: null, goType: goType, - optional: false, json: `\`json:"${id.text}"\``, - gostuff: `${getText(node)}` + optional: false, json: `\`json:"${id.text}"\``, + gostuff: `${getText(node)}` } } @@ -662,13 +670,12 @@ function generate(files: string[], options: ts.CompilerOptions): void { if (s.charAt(0) == '_') { ans = 'Inner' + s.substring(1) } - else { ans = s.substring(0, 1).toUpperCase() + s.substring(1) }; + else {ans = s.substring(0, 1).toUpperCase() + s.substring(1)}; ans = ans.replace(/Uri$/, 'URI') ans = ans.replace(/Id$/, 'ID') return ans } - // find the text of a node function getText(node: ts.Node): string { let sf = node.getSourceFile(); @@ -678,12 +685,39 @@ function generate(files: string[], options: ts.CompilerOptions): void { } // return a string of the kinds of the immediate descendants function kinds(n: ts.Node): string { - let res = 'Seen ' + ts.SyntaxKind[n.kind]; - function f(n: ts.Node): void { res += ' ' + ts.SyntaxKind[n.kind] }; + let res = 'Seen ' + strKind(n); + function f(n: ts.Node): void{res += ' ' + strKind(n)}; ts.forEachChild(n, f) return res } + function strKind(n: ts.Node): string { + const x = ts.SyntaxKind[n.kind]; + // some of these have two names + switch (x) { + default: + return x; + case 'FirstAssignment': + return 'EqualsToken'; + case 'FirstBinaryOperator': + return 'LessThanToken'; + case 'FirstCompoundAssignment': + return 'PlusEqualsToken'; + case 'FirstContextualKeyword': + return 'AbstractKeyword'; + case 'FirstLiteralToken': + return 'NumericLiteral'; + case 'FirstNode': + return 'QualifiedName'; + case 'FirstTemplateToken': + return 'NoSubstitutionTemplateLiteral'; + case 'LastTemplateToken': + return 'TemplateTail'; + case 'FirstTypeNode': + return 'TypePredicate'; + } + } + function describe(node: ts.Node) { if (node === undefined) { return @@ -693,36 +727,22 @@ function generate(files: string[], options: ts.CompilerOptions): void { function f(n: ts.Node) { seenAdd(kinds(n)) if (ts.isIdentifier(n)) { - pra(`${indent} ${loc(n)} ${ts.SyntaxKind[n.kind]} ${n.text}\n`) + pra(`${indent} ${loc(n)} ${strKind(n)} ${n.text}\n`) } else if (ts.isPropertySignature(n) || ts.isEnumMember(n)) { - pra(`${indent} ${loc(n)} ${ts.SyntaxKind[n.kind]}\n`) + pra(`${indent} ${loc(n)} ${strKind(n)}\n`) } else if (ts.isTypeLiteralNode(n)) { let m = n.members - pra(`${indent} ${loc(n)} ${ts.SyntaxKind[n.kind]} ${m.length}\n`) + pra(`${indent} ${loc(n)} ${strKind(n)} ${m.length}\n`) } - else { pra(`${indent} ${loc(n)} ${ts.SyntaxKind[n.kind]}\n`) }; + else {pra(`${indent} ${loc(n)} ${strKind(n)}\n`)}; indent += ' ' ts.forEachChild(n, f) indent = indent.slice(0, indent.length - 2) } f(node) } - - - function loc(node: ts.Node): string { - const sf = node.getSourceFile() - const start = node.getStart() - const x = sf.getLineAndCharacterOfPosition(start) - const full = node.getFullStart() - const y = sf.getLineAndCharacterOfPosition(full) - let fn = sf.fileName - const n = fn.search(/-node./) - fn = fn.substring(n + 6) - return `${fn} ${x.line + 1}:${x.character + 1} (${y.line + 1}:${ - y.character + 1})` - } } function getComments(node: ts.Node): string { @@ -733,157 +753,259 @@ function getComments(node: ts.Node): string { return x } +function loc(node: ts.Node): string{const sf = node.getSourceFile() +const start = node.getStart() +const x = sf.getLineAndCharacterOfPosition(start) +const full = node.getFullStart() +const y = sf.getLineAndCharacterOfPosition(full) +let fn = sf.fileName +const n = fn.search(/-node./) +fn = fn.substring(n + 6) + return `${fn} ${x.line + 1}:${x.character + 1} (${y.line + 1}:${ + y.character + 1})` +} + function emitTypes() { + seenConstTypes.set('MessageQueue', true); // skip for (const t of Types) { + if (seenConstTypes.get(t.goName)) continue; if (t.goName == 'CodeActionKind') continue; // consts better choice + if (t.goType === undefined) continue; let stuff = (t.stuff == undefined) ? '' : t.stuff; prgo(`// ${t.goName} is a type\n`) prgo(`${getComments(t.me)}`) prgo(`type ${t.goName} ${t.goType}${stuff}\n`) + seenConstTypes.set(t.goName, true); } } -function emitStructs() { - let seenName = new Map() - for (const str of Structs) { - if (str.name == 'InitializeError') { - // only want the consts - continue +let byName = new Map(); + function emitStructs() { + dontEmit.set('Thenable', true); + dontEmit.set('EmitterOptions', true); + dontEmit.set('MessageReader', true); + dontEmit.set('MessageWriter', true); + dontEmit.set('CancellationToken', true); + dontEmit.set('PipeTransport', true); + dontEmit.set('SocketTransport', true); + dontEmit.set('Item', true); + dontEmit.set('Event', true); + dontEmit.set('Logger', true); + dontEmit.set('Disposable', true); + dontEmit.set('PartialMessageInfo', true); + dontEmit.set('MessageConnection', true); + dontEmit.set('ResponsePromise', true); + dontEmit.set('ResponseMessage', true); + dontEmit.set('ErrorMessage', true); + dontEmit.set('NotificationMessage', true); + dontEmit.set('RequestHandlerElement', true); + dontEmit.set('RequestMessage', true); + dontEmit.set('NotificationHandlerElement', true); + dontEmit.set('Message', true); // duplicate of jsonrpc2:wire.go + dontEmit.set('LSPLogMessage', true); + dontEmit.set('InnerEM', true); + dontEmit.set('ResponseErrorLiteral', true); + dontEmit.set('TraceOptions', true); + dontEmit.set('MessageType', true); // want the enum + // backwards compatibility, done in requests.ts: + dontEmit.set('CancelParams', true); + + for (const str of Structs) { + byName.set(str.name, str) } - if (seenName[str.name]) { - continue + let seenName = new Map() + for (const str of Structs) { + if (str.name == 'InitializeError') { + // only want its consts, not the struct + continue + } + if (seenName.get(str.name) || dontEmit.get(str.name)) { + continue + } + let noopt = false; + seenName.set(str.name, true) + prgo(genComments(str.name, getComments(str.me))) + prgo(`type ${str.name} struct {\n`) + // if it has fields, generate them + if (str.fields != undefined) { + for (const f of str.fields) { + prgo(strField(f, noopt)) + } + } + if (str.extends) { + // ResourceOperation just repeats the Kind field + for (const s of str.extends) { + if (s != 'ResourceOperation') + prgo(`\t${s}\n`) // what this type extends. + } + } else if (str.embeds) { + prb(`embeds: ${str.name}\n`); + noopt = (str.name == 'ClientCapabilities'); + // embedded struct. the hard case is from intersection types, + // where fields with the same name have to be combined into + // a single struct + let fields = new Map(); + for (const e of str.embeds) { + const nm = byName.get(e); + if (nm.embeds) throw new Error(`${nm.name} is an embedded embed`); + // each of these fields might be a something that needs amalgamating + for (const f of nm.fields) { + let x = fields.get(f.goName); + if (x === undefined) x = []; + x.push(f); + fields.set(f.goName, x); + } + } + fields.forEach((val) => { + if (val.length > 1) { + // merge the fields with the same name + prgo(strField(val[0], noopt, val)); + } else { + prgo(strField(val[0], noopt)); + } + }); + } + prgo(`}\n`); } - seenName[str.name] = true - prgo(genComments(str.name, getComments(str.me))) - /* prgo(`// ${str.name} is:\n`) - prgo(getComments(str.me))*/ - prgo(`type ${str.name} struct {\n`) - for (const s of str.embeds) { - prgo(`\t${s}\n`) + } + + function genComments(name: string, maybe: string): string { + if (maybe == '') return `\n\t// ${name} is\n`; + if (maybe.indexOf('/**') == 0) { + return maybe.replace('/**', `\n/*${name} defined:`) } - if (str.fields != undefined) { - for (const f of str.fields) { - prgo(strField(f)) + throw new Error(`weird comment ${maybe.indexOf('/**')}`) + } + + // Turn a Field into an output string + function strField(f: Field, noopt?: boolean, flds?: Field[]): string { + let ans: string[] = []; + let opt = (!noopt && f.optional) ? '*' : '' + switch (f.goType.charAt(0)) { + case 's': // string + case 'b': // bool + case 'f': // float64 + case 'i': // interface{} + case '[': // []foo + opt = '' + } + let stuff = (f.gostuff == undefined) ? '' : ` // ${f.gostuff}` + ans.push(genComments(f.goName, getComments(f.me))) + if (flds === undefined && f.substruct == undefined) { + ans.push(`\t${f.goName} ${opt}${f.goType} ${f.json}${stuff}\n`) + } + else if (flds !== undefined) { + ans.push(`\t${f.goName} ${opt}struct{\n`); + for (const ff of flds) { + if (ff.substruct !== undefined) { + for (const x of ff.substruct) { + ans.push(strField(x, noopt)) + } + } else if (byName.get(ff.goType) !== undefined) { + const st = byName.get(ff.goType); + for (let i = 0; i < st.fields.length; i++) { + ans.push(strField(st.fields[i], noopt)) + } + } else { + ans.push(strField(ff, noopt)); + } + } + ans.push(`\t} ${f.json}${stuff}\n`); + } + else { + ans.push(`\t${f.goName} ${opt}struct {\n`) + for (const x of f.substruct) { + ans.push(strField(x, noopt)) + } + ans.push(`\t} ${f.json}${stuff}\n`) + } + return (''.concat(...ans)) + } + + function emitConsts() { + // need the consts too! Generate modifying prefixes and suffixes to ensure + // consts are unique. (Go consts are package-level, but Typescript's are + // not.) Use suffixes to minimize changes to gopls. + let pref = new Map( + [['DiagnosticSeverity', 'Severity']]) // typeName->prefix + let suff = new Map([ + ['CompletionItemKind', 'Completion'], ['InsertTextFormat', 'TextFormat'] + ]) + for (const c of Consts) { + if (seenConstTypes.get(c.typeName)) { + continue + } + seenConstTypes.set(c.typeName, true); + if (pref.get(c.typeName) == undefined) { + pref.set(c.typeName, '') // initialize to empty value + } + if (suff.get(c.typeName) == undefined) { + suff.set(c.typeName, '') + } + prgo(`// ${c.typeName} defines constants\n`) + prgo(`type ${c.typeName} ${c.goType}\n`) + } + prgo('const (\n') + let seenConsts = new Map() // to avoid duplicates + for (const c of Consts) { + const x = `${pref.get(c.typeName)}${c.name}${suff.get(c.typeName)}` + if (seenConsts.get(x)) { + continue + } + seenConsts.set(x, true) + if (c.value === undefined) continue; // didn't figure it out + if (x.startsWith('undefined')) continue; // what's going on here? + prgo(genComments(x, getComments(c.me))) + prgo(`\t${x} ${c.typeName} = ${c.value}\n`) + } + prgo(')\n') + } + + function emitHeader(files: string[]) { + let lastMod = 0 + let lastDate: Date + for (const f of files) { + const st = fs.statSync(f) + if (st.mtimeMs > lastMod) { + lastMod = st.mtimeMs + lastDate = st.mtime } } - prgo(`}\n`) - } -} + let a = fs.readFileSync(`${dir}${srcDir}/.git/refs/heads/master`); + prgo(`// Package protocol contains data types and code for LSP jsonrpcs\n`) + prgo(`// generated automatically from vscode-languageserver-node\n`) + prgo(`// commit: ${a.toString()}`) + prgo(`// last fetched ${lastDate}\n`) + prgo('package protocol\n\n') + prgo(`// Code generated (see typescript/README.md) DO NOT EDIT.\n`); + }; -function genComments(name: string, maybe: string): string { - if (maybe == '') return `\n\t// ${name} is\n`; - if (maybe.indexOf('/**') == 0) { - return maybe.replace('/**', `\n/*${name} defined:`) + // ad hoc argument parsing: [-d dir] [-o outputfile], and order matters + function main() { + let args = process.argv.slice(2) // effective command line + if (args.length > 0) { + let j = 0; + if (args[j] == '-d') { + dir = args[j + 1] + j += 2 + } + if (args[j] == '-o') { + outFname = args[j + 1] + j += 2 + } + if (j != args.length) throw new Error(`incomprehensible args ${args}`) + } + let files: string[] = []; + for (let i = 0; i < fnames.length; i++) { + files.push(`${dir}${fnames[i]}`) + } + createOutputFiles() + generate( + files, {target: ts.ScriptTarget.ES5, module: ts.ModuleKind.CommonJS}); + emitHeader(files) + emitStructs() + emitConsts() + emitTypes() } - throw new Error(`weird comment ${maybe.indexOf('/**')}`) -} -// Turn a Field into an output string -function strField(f: Field): string { - let ans: string[] = []; - let opt = f.optional ? '*' : '' - switch (f.goType.charAt(0)) { - case 's': // string - case 'b': // bool - case 'f': // float64 - case 'i': // interface{} - case '[': // []foo - opt = '' - } - let stuff = (f.gostuff == undefined) ? '' : ` // ${f.gostuff}` - ans.push(genComments(f.goName, getComments(f.me))) - if (f.substruct == undefined) { - ans.push(`\t${f.goName} ${opt}${f.goType} ${f.json}${stuff}\n`) - } - else { - ans.push(`\t${f.goName} ${opt}struct {\n`) - for (const x of f.substruct) { - ans.push(strField(x)) - } - ans.push(`\t} ${f.json}${stuff}\n`) - } - return (''.concat(...ans)) -} - -function emitConsts() { - // Generate modifying prefixes and suffixes to ensure consts are - // unique. (Go consts are package-level, but Typescript's are not.) - // Use suffixes to minimize changes to gopls. - let pref = new Map( - [['DiagnosticSeverity', 'Severity']]) // typeName->prefix - let suff = new Map([ - ['CompletionItemKind', 'Completion'], ['InsertTextFormat', 'TextFormat'] - ]) - for (const c of Consts) { - if (seenConstTypes[c.typeName]) { - continue - } - seenConstTypes[c.typeName] = true - if (pref.get(c.typeName) == undefined) { - pref.set(c.typeName, '') // initialize to empty value - } - if (suff.get(c.typeName) == undefined) { - suff.set(c.typeName, '') - } - prgo(`// ${c.typeName} defines constants\n`) - prgo(`type ${c.typeName} ${c.goType}\n`) - } - prgo('const (\n') - let seenConsts = new Map() // to avoid duplicates - for (const c of Consts) { - const x = `${pref.get(c.typeName)}${c.name}${suff.get(c.typeName)}` - if (seenConsts.get(x)) { - continue - } - seenConsts.set(x, true) - prgo(genComments(x, getComments(c.me))) - prgo(`\t${x} ${c.typeName} = ${c.value}\n`) - } - prgo(')\n') -} - -function emitHeader(files: string[]) { - let lastMod = 0 - let lastDate: Date - for (const f of files) { - const st = fs.statSync(f) - if (st.mtimeMs > lastMod) { - lastMod = st.mtimeMs - lastDate = st.mtime - } - } - prgo(`// Package protocol contains data types for LSP jsonrpcs\n`) - prgo(`// generated automatically from vscode-languageserver-node - // version of ${lastDate}\n`) - prgo('package protocol\n\n') -}; - -// ad hoc argument parsing: [-d dir] [-o outputfile], and order matters -function main() { - let args = process.argv.slice(2) // effective command line - if (args.length > 0) { - let j = 0; - if (args[j] == '-d') { - dir = args[j + 1] - j += 2 - } - if (args[j] == '-o') { - outFname = args[j + 1] - j += 2 - } - if (j != args.length) throw new Error(`incomprehensible args ${args}`) - } - let files: string[] = []; - for (let i = 0; i < fnames.length; i++) { - files.push(`${dir}${fnames[i]}`) - } - createOutputFiles() - generate( - files, { target: ts.ScriptTarget.ES5, module: ts.ModuleKind.CommonJS }); - emitHeader(files) - emitStructs() - emitConsts() - emitTypes() -} - -main() + main() diff --git a/internal/lsp/protocol/typescript/requests.ts b/internal/lsp/protocol/typescript/requests.ts new file mode 100644 index 00000000..53063f43 --- /dev/null +++ b/internal/lsp/protocol/typescript/requests.ts @@ -0,0 +1,472 @@ +import * as fs from 'fs'; +import * as ts from 'typescript'; + +// generate tsclient.go and tsserver.go, which are the definitions and stubs for +// supporting the LPS protocol. These files have 3 sections: +// 1. define the Client or Server type +// 2. fill out the clientHandler or serveHandler which is basically a large +// switch on the requests and notifications received by the client/server. +// 3. The methods corresponding to these. (basically parse the request, +// call something, and perhaps send a response.) + +let dir = process.env['HOME']; +let fnames = [ + `/vscode-languageserver-node/protocol/src/protocol.ts`, + `/vscode-languageserver-node/jsonrpc/src/main.ts` +]; + +let fda: number, fdy: number; // file descriptors + +function createOutputFiles() { + fda = fs.openSync('/tmp/ts-a', 'w') // dump of AST + fdy = fs.openSync('/tmp/ts-c', 'w') // unused, for debugging +} +function pra(s: string) { + return (fs.writeSync(fda, s)) +} +function prb(s: string) { + return (fs.writeSync(fdy, s + '\n')) +} + +let program: ts.Program; +function generate(files: string[], options: ts.CompilerOptions): void { + program = ts.createProgram(files, options); + program.getTypeChecker(); + + dumpAST(); // for debugging + + // visit every sourceFile in the program, collecting information + for (const sourceFile of program.getSourceFiles()) { + if (!sourceFile.isDeclarationFile) { + ts.forEachChild(sourceFile, genStuff) + } + } + // when 4 args, they are param, result, error, registration options, e.g.: + // RequestType('textDocument/implementation'); + // 3 args is RequestType0('shutdown') + // and RequestType0('workspace/workspaceFolders) + + // the two args are the notification data and the registration data + // except for textDocument/selectionRange and a NotificationType0('exit') + // selectionRange is the following, but for now do it by hand, special case. + // RequestType + // = new RequestType('textDocument/selectionRange') + // and foldingRange has the same problem. + + setReceives(); // distinguish client and server + // for each of Client and Server there are 3 parts to the output: + // 1. type X interface {methods} + // 2. serverHandler(...) { return func(...) { switch r.method}} + // 3. func (x *xDispatcher) Method(ctx, parm) + not.forEach( + (v, k) => {receives.get(k) == 'client' ? goNot(client, k) : + goNot(server, k)}); + req.forEach( + (v, k) => {receives.get(k) == 'client' ? goReq(client, k) : + goReq(server, k)}); + // and print the Go code + output(client); + output(server); + return; +} + +// Go signatures for methods. +function sig(nm: string, a: string, b: string, names?: boolean): string { + if (a != '') { + if (names) + a = ', params *' + a; + else + a = ', *' + a; + } + let ret = 'error'; + if (b != '') { + b.startsWith('[]') || b.startsWith('interface') || (b = '*' + b); + ret = `(${b}, error)`; + } + let start = `${nm}(`; + if (names) { + start = start + 'ctx '; + } + return `${start}context.Context${a}) ${ret}`; +} + +const notNil = `if r.Params != nil { + conn.Reply(ctx, r, nil, jsonrpc2.NewErrorf(jsonrpc2.CodeInvalidParams, "Expected no params")) + return + }`; +// Go code for notifications. Side is client or server, m is the request method +function goNot(side: side, m: string) { + const n = not.get(m); + let a = goType(m, n.typeArguments[0]); + // let b = goType(m, n.typeArguments[1]); These are registration options + const nm = methodName(m); + side.methods.push(sig(nm, a, '')); + const caseHdr = `case "${m}": // notif`; + let case1 = notNil; + if (a != '') { + case1 = `var params ${a} + if err := json.Unmarshal(*r.Params, ¶ms); err != nil { + sendParseError(ctx, log, conn, r, err) + return + } + if err := ${side.name}.${nm}(ctx, ¶ms); err != nil { + log.Errorf(ctx, "%v", err) + }`; + } else { + case1 = `if err := ${side.name}.${nm}(ctx); err != nil { + log.Errorf(ctx, "%v", err) + }`; + } + side.cases.push(`${caseHdr}\n${case1}`); + + const arg3 = a == '' ? 'nil' : 'params'; + side.calls.push(` + func (s *${side.name}Dispatcher) ${sig(nm, a, '', true)} { + return s.Conn.Notify(ctx, "${m}", ${arg3}) + }`); +} + +// Go code for requests. +function goReq(side: side, m: string) { + const n = req.get(m); + + const nm = methodName(m); + let a = goType(m, n.typeArguments[0]); + let b = goType(m, n.typeArguments[1]); + if (n.getText().includes('Type0')) { + b = a; + a = ''; // workspace/workspaceFolders and shutdown + } + prb(`${side.name} req ${a != ''},${b != ''} ${nm} ${m} ${loc(n)}`) + side.methods.push(sig(nm, a, b)); + + const caseHdr = `case "${m}": // req`; + let case1 = notNil; + if (a != '') { + case1 = `var params ${a} + if err := json.Unmarshal(*r.Params, ¶ms); err != nil { + sendParseError(ctx, log, conn, r, err) + return + }`; + } + const arg2 = a == '' ? '' : ', ¶ms'; + let case2 = `if err := ${side.name}.${nm}(ctx${arg2}); err != nil { + log.Errorf(ctx, "%v", err) + }`; + if (b != '') { + case2 = `resp, err := ${side.name}.${nm}(ctx${arg2}) + if err := conn.Reply(ctx, r, resp, err); err != nil { + log.Errorf(ctx, "%v", err) + }`; + } else { // response is nil + case2 = `err := ${side.name}.${nm}(ctx${arg2}) + if err := conn.Reply(ctx, r, nil, err); err != nil { + log.Errorf(ctx, "%v", err) + }` + } + + side.cases.push(`${caseHdr}\n${case1}\n${case2}`); + + const callHdr = `func (s *${side.name}Dispatcher) ${sig(nm, a, b, true)} {`; + let callBody = `return s.Conn.Call(ctx, "${m}", nil, nil)\n}`; + if (b != '') { + const p2 = a == '' ? 'nil' : 'params'; + let theRet = `result`; + !b.startsWith('[]') && !b.startsWith('interface') && (theRet = '&result'); + callBody = `var result ${b} + if err := s.Conn.Call(ctx, "${m}", ${ + p2}, &result); err != nil { + return nil, err + } + return ${theRet}, nil + }`; + } else if (a != '') { + callBody = `return s.Conn.Call(ctx, "${m}", params, nil) // Call, not Notify + }` + } + side.calls.push(`${callHdr}\n${callBody}\n`); +} + +// make sure method names are unique +let seenNames = new Set(); +function methodName(m: string): string { + const i = m.indexOf('/'); + let s = m.substring(i + 1); + let x = s[0].toUpperCase() + s.substring(1); + if (seenNames.has(x)) { + x += m[0].toUpperCase() + m.substring(1, i); + } + seenNames.add(x); + return x; +} + +function output(side: side) { + if (side.outputFile === undefined) side.outputFile = `ts${side.name}.go`; + side.fd = fs.openSync(side.outputFile, 'w'); + const f = function(s: string) { + fs.writeSync(side.fd, s); + fs.writeSync(side.fd, '\n'); + }; + f(`package protocol`); + f(`// Code generated (see typescript/README.md) DO NOT EDIT.\n`); + f(` + import ( + "context" + "encoding/json" + + "golang.org/x/tools/internal/jsonrpc2" + "golang.org/x/tools/internal/lsp/xlog" + ) + `); + const a = side.name[0].toUpperCase() + side.name.substring(1) + f(`type ${a} interface {`); + side.methods.forEach((v) => {f(v)}); + f('}\n'); + f(`func ${side.name}Handler(log xlog.Logger, ${side.name} ${ + side.goName}) jsonrpc2.Handler { + return func(ctx context.Context, conn *jsonrpc2.Conn, r *jsonrpc2.Request) { + switch r.Method { + case "$/cancelRequest": + var params CancelParams + if err := json.Unmarshal(*r.Params, ¶ms); err != nil { + sendParseError(ctx, log, conn, r, err) + return + } + conn.Cancel(params.ID)`); + side.cases.forEach((v) => {f(v)}); + f(` + default: + if r.IsNotify() { + conn.Reply(ctx, r, nil, jsonrpc2.NewErrorf(jsonrpc2.CodeMethodNotFound, "method %q not found", r.Method)) + } + } +} +}`); + f(` + type ${side.name}Dispatcher struct { + *jsonrpc2.Conn + } + `); + side.calls.forEach((v) => {f(v)}); + if (side.name == 'server') + f(` + type CancelParams struct { + /** + * The request id to cancel. + */ + ID jsonrpc2.ID \`json:"id"\` + }`); +} + +interface side { + methods: string[]; + cases: string[]; + calls: string[]; + name: string; // client or server + goName: string; // Client or Server + outputFile?: string; + fd?: number +} +let client: side = + {methods: [], cases: [], calls: [], name: 'client', goName: 'Client'}; +let server: side = + {methods: [], cases: [], calls: [], name: 'server', goName: 'Server'}; + +let req = new Map(); // requests +let not = new Map(); // notifications +let receives = new Map(); // who receives it + +function setReceives() { + // mark them all as server, then adjust the client ones. + // it would be nice to have some independent check + req.forEach((_, k) => {receives.set(k, 'server')}); + not.forEach((_, k) => {receives.set(k, 'server')}); + receives.set('window/logMessage', 'client'); + receives.set('telemetry/event', 'client'); + receives.set('client/registerCapability', 'client'); + receives.set('client/unregisterCapability', 'client'); + receives.set('window/showMessage', 'client'); + receives.set('window/showMessageRequest', 'client'); + receives.set('workspace/workspaceFolders', 'client'); + receives.set('workspace/configuration', 'client'); + receives.set('workspace/applyEdit', 'client'); + receives.set('textDocument/publishDiagnostics', 'client'); + // a small check + receives.forEach((_, k) => { + if (!req.get(k) && !not.get(k)) throw new Error(`missing ${k}}`); + if (req.get(k) && not.get(k)) throw new Error(`dup ${k}`); + }) +} + +function goType(m: string, n: ts.Node): string { + if (n === undefined) return ''; + if (ts.isTypeReferenceNode(n)) return n.typeName.getText(); + if (n.kind == ts.SyntaxKind.VoidKeyword) return ''; + if (n.kind == ts.SyntaxKind.AnyKeyword) return 'interface{}'; + if (ts.isArrayTypeNode(n)) return '[]' + goType(m, n.elementType); + // special cases, before we get confused + switch (m) { + case 'textDocument/completion': + return 'CompletionList'; + case 'textDocument/documentSymbol': + return '[]DocumentSymbol'; + case 'textDocument/prepareRename': + return 'Range'; + case 'textDocument/codeAction': + return '[]CodeAction'; + } + if (ts.isUnionTypeNode(n)) { + let x: string[] = []; + n.types.forEach( + (v) => {v.kind != ts.SyntaxKind.NullKeyword && x.push(goType(m, v))}); + if (x.length == 1) return x[0]; + + prb(`===========${m} ${x}`) + // Because we don't fully resolve types, we don't know that + // Definition is Location | Location[] + if (x[0] == 'Definition') return '[]Location'; + if (x[1] == '[]' + x[0] + 'Link') return x[1]; + throw new Error(`${m}, ${x} unexpected types`) + } + return '?'; +} + +// walk the AST finding Requests and Notifications +function genStuff(node: ts.Node) { + if (!ts.isNewExpression(node)) { + ts.forEachChild(node, genStuff) + return; + } + // process the right kind of new expression + const wh = node.expression.getText(); + if (wh != 'RequestType' && wh != 'RequestType0' && wh != 'NotificationType' && + wh != 'NotificationType0') + return; + if (node.arguments === undefined || node.arguments.length != 1 || + !ts.isStringLiteral(node.arguments[0])) { + throw new Error(`missing n.arguments ${loc(node)}`) + } + // RequestType=new RequestTYpe('foo') + if (node.typeArguments === undefined) { + node.typeArguments = lookUp(node); + } + // new RequestType + let s = node.arguments[0].getText(); + // Request or Notification + const v = wh[0] == 'R' ? req : not; + s = s.substring(1, s.length - 1); // remove quoting + if (s == '$/cancelRequest') return; // special case in output + v.set(s, node); +} + +function lookUp(n: ts.NewExpression): ts.NodeArray { + // parent should be VariableDeclaration. its children should be + // Identifier('type') ??? + // TypeReference: [Identifier('RequestType1), ] + // NewExpression (us) + const p = n.parent; + if (!ts.isVariableDeclaration(p)) throw new Error(`not variable decl`); + const tr = p.type; + if (!ts.isTypeReferenceNode(tr)) throw new Error(`not TypeReference`); + return tr.typeArguments; +} + +function dumpAST() { + // dump the ast, for debugging + for (const sourceFile of program.getSourceFiles()) { + if (!sourceFile.isDeclarationFile) { + // walk the tree to do stuff + ts.forEachChild(sourceFile, describe); + } + } +} + +// some tokens have the wrong default name +function strKind(n: ts.Node): string { + const x = ts.SyntaxKind[n.kind]; + switch (x) { + default: + return x; + case 'FirstAssignment': + return 'EqualsToken'; + case 'FirstBinaryOperator': + return 'LessThanToken'; + case 'FirstCompoundAssignment': + return 'PlusEqualsToken'; + case 'FirstContextualKeyword': + return 'AbstractKeyword'; + case 'FirstLiteralToken': + return 'NumericLiteral'; + case 'FirstNode': + return 'QualifiedName'; + case 'FirstTemplateToken': + return 'NoSubstitutionTemplateLiteral'; + case 'LastTemplateToken': + return 'TemplateTail'; + case 'FirstTypeNode': + return 'TypePredicate'; + } +} + +function describe(node: ts.Node) { + if (node === undefined) { + return + } + let indent = ''; + + function f(n: ts.Node) { + if (ts.isIdentifier(n)) { + pra(`${indent} ${loc(n)} ${strKind(n)} ${n.text} \n`) + } else if (ts.isPropertySignature(n) || ts.isEnumMember(n)) { + pra(`${indent} ${loc(n)} ${strKind(n)} \n`) + } else if (ts.isTypeLiteralNode(n)) { + let m = n.members + pra(`${indent} ${loc(n)} ${strKind(n)} ${m.length} \n`) + } else { + pra(`${indent} ${loc(n)} ${strKind(n)} \n`) + }; + indent += ' ' + ts.forEachChild(n, f) + indent = indent.slice(0, indent.length - 2) + } + f(node) +} + +// string version of the location in the source file +function loc(node: ts.Node): string { + const sf = node.getSourceFile(); + const start = node.getStart() + const x = sf.getLineAndCharacterOfPosition(start) + const full = node.getFullStart() + const y = sf.getLineAndCharacterOfPosition(full) + let fn = sf.fileName + const n = fn.search(/-node./) + fn = fn.substring(n + 6) + return `${fn} ${x.line + 1}: ${x.character + 1} (${y.line + 1}: ${ + y.character + 1})` +} + +// ad hoc argument parsing: [-d dir] [-o outputfile], and order matters +function main() { + let args = process.argv.slice(2) // effective command line + if (args.length > 0) { + let j = 0; + if (args[j] == '-d') { + dir = args[j + 1] + j += 2 + } + if (j != args.length) throw new Error(`incomprehensible args ${args}`) + } + let files: string[] = []; + for (let i = 0; i < fnames.length; i++) { + files.push(`${dir}${fnames[i]}`) + } + createOutputFiles() + generate( + files, {target: ts.ScriptTarget.ES5, module: ts.ModuleKind.CommonJS}); +} + +main()