internal/lsp/protocol: bring the code generating programs up to date
This is the typescript code that generates the current versions of
tsprotocol.go, tsserver.go, and tsclient.go.
Change-Id: If40cd7a46e5e7d646d99670da5e04831b6ddc222
Reviewed-on: https://go-review.googlesource.com/c/tools/+/180477
Reviewed-by: Ian Cottrell <iancottrell@google.com>
diff --git a/internal/lsp/protocol/typescript/README.md b/internal/lsp/protocol/typescript/README.md
index 92f52ac..4084c4f 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](<https://nodejs.org> 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 @@
`-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](<https://github.com/Microsoft/TypeScript/wiki/Architectural-Overview> 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 c78728f..09f4fb2 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 @@
interface Struct {
me: ts.Node
name: string
- embeds: string[]
- fields?: Field[]
+ embeds?: string[]
+ fields?: Field[];
+ extends?: string[]
}
let Structs: Struct[] = [];
@@ -46,9 +49,10 @@
}
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 @@
return (fs.writeSync(fde, s))
}
+// struct names that don't need to go in the output
+let dontEmit = new Map<string, boolean>();
+
function generate(files: string[], options: ts.CompilerOptions): void {
let program = ts.createProgram(files, options);
program.getTypeChecker(); // used for side-effects
@@ -93,11 +100,11 @@
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 @@
} 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<ts.PropertyDeclaration>()
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 @@
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 @@
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 @@
// 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<ts.PropertySignature>()
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 @@
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 @@
const ans = {
me: node,
name: toGoName(getText(id)),
- embeds: heritageStrs(extend),
+ extends: heritageStrs(extend),
fields: fields
};
@@ -287,7 +289,7 @@
}
})
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 @@
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 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 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 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 @@
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 @@
// 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 // <T>, 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 @@
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 @@
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 @@
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 @@
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<TypeNode>;typeName: EntityName;
// typeArguments won't show up in the generated Go
@@ -543,10 +540,10 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
}
// 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 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 @@
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<string, boolean>()
- for (const str of Structs) {
- if (str.name == 'InitializeError') {
- // only want the consts
- continue
+let byName = new Map<string, Struct>();
+ 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<string, boolean>()
+ 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<string, Field[]>();
+ 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<string, string>(
+ [['DiagnosticSeverity', 'Severity']]) // typeName->prefix
+ let suff = new Map<string, string>([
+ ['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<string, boolean>() // 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<string, string>(
- [['DiagnosticSeverity', 'Severity']]) // typeName->prefix
- let suff = new Map<string, string>([
- ['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<string, boolean>() // 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 0000000..53063f4
--- /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<TextDocumentPositionParams, Definition | DefinitionLink[] |
+ // null,
+ // void, TextDocumentRegistrationOptions>('textDocument/implementation');
+ // 3 args is RequestType0('shutdown')<void, void, void>
+ // and RequestType0('workspace/workspaceFolders)<WorkspaceFolder[]|null, void,
+ // void>
+
+ // 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<TextDocumentPositionParams, SelectionRange[] | null, any, any>
+ // = 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<string>();
+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<string, ts.NewExpression>(); // requests
+let not = new Map<string, ts.NewExpression>(); // notifications
+let receives = new Map<string, 'server'|'client'>(); // 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<useful>=new RequestTYpe('foo')
+ if (node.typeArguments === undefined) {
+ node.typeArguments = lookUp(node);
+ }
+ // new RequestType<useful>
+ 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<ts.TypeNode> {
+ // 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()