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>
This commit is contained in:
parent
d4e310b4a8
commit
027b3b4d7b
|
@ -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 @@ 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.
|
||||
`go.ts` uses the Typescript compiler's API, which is [introduced](<https://github.com/Microsoft/TypeScript/wiki/Architectural-Overview> API) in their wiki.
|
|
@ -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<string, boolean>();
|
|||
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<string, boolean>();
|
||||
|
||||
function generate(files: string[], options: ts.CompilerOptions): void {
|
||||
let program = ts.createProgram(files, options);
|
||||
program.getTypeChecker(); // used for side-effects
|
||||
|
@ -93,7 +100,7 @@ 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) ||
|
||||
|
@ -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<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 @@ 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<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 @@ 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,11 +303,12 @@ 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 '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 // <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 @@ 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
|
||||
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<TypeNode>;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 })
|
||||
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 {
|
||||
|
@ -637,7 +646,7 @@ function generate(files: string[], options: ts.CompilerOptions): void {
|
|||
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,8 +654,7 @@ 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}`
|
||||
|
@ -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,55 +753,134 @@ 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 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)
|
||||
}
|
||||
let seenName = new Map<string, boolean>()
|
||||
for (const str of Structs) {
|
||||
if (str.name == 'InitializeError') {
|
||||
// only want the consts
|
||||
// only want its consts, not the struct
|
||||
continue
|
||||
}
|
||||
if (seenName[str.name]) {
|
||||
if (seenName.get(str.name) || dontEmit.get(str.name)) {
|
||||
continue
|
||||
}
|
||||
seenName[str.name] = true
|
||||
let noopt = false;
|
||||
seenName.set(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`)
|
||||
}
|
||||
// if it has fields, generate them
|
||||
if (str.fields != undefined) {
|
||||
for (const f of str.fields) {
|
||||
prgo(strField(f))
|
||||
prgo(strField(f, noopt))
|
||||
}
|
||||
}
|
||||
prgo(`}\n`)
|
||||
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`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function genComments(name: string, maybe: string): string {
|
||||
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:`)
|
||||
}
|
||||
throw new Error(`weird comment ${maybe.indexOf('/**')}`)
|
||||
}
|
||||
}
|
||||
|
||||
// Turn a Field into an output string
|
||||
function strField(f: Field): string {
|
||||
// Turn a Field into an output string
|
||||
function strField(f: Field, noopt?: boolean, flds?: Field[]): string {
|
||||
let ans: string[] = [];
|
||||
let opt = f.optional ? '*' : ''
|
||||
let opt = (!noopt && f.optional) ? '*' : ''
|
||||
switch (f.goType.charAt(0)) {
|
||||
case 's': // string
|
||||
case 'b': // bool
|
||||
|
@ -792,33 +891,51 @@ function strField(f: Field): string {
|
|||
}
|
||||
let stuff = (f.gostuff == undefined) ? '' : ` // ${f.gostuff}`
|
||||
ans.push(genComments(f.goName, getComments(f.me)))
|
||||
if (f.substruct == undefined) {
|
||||
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))
|
||||
ans.push(strField(x, noopt))
|
||||
}
|
||||
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.
|
||||
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[c.typeName]) {
|
||||
if (seenConstTypes.get(c.typeName)) {
|
||||
continue
|
||||
}
|
||||
seenConstTypes[c.typeName] = true
|
||||
seenConstTypes.set(c.typeName, true);
|
||||
if (pref.get(c.typeName) == undefined) {
|
||||
pref.set(c.typeName, '') // initialize to empty value
|
||||
}
|
||||
|
@ -836,13 +953,15 @@ function emitConsts() {
|
|||
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[]) {
|
||||
function emitHeader(files: string[]) {
|
||||
let lastMod = 0
|
||||
let lastDate: Date
|
||||
for (const f of files) {
|
||||
|
@ -852,14 +971,17 @@ function emitHeader(files: string[]) {
|
|||
lastDate = st.mtime
|
||||
}
|
||||
}
|
||||
prgo(`// Package protocol contains data types for LSP jsonrpcs\n`)
|
||||
prgo(`// generated automatically from vscode-languageserver-node
|
||||
// version of ${lastDate}\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`);
|
||||
};
|
||||
|
||||
// ad hoc argument parsing: [-d dir] [-o outputfile], and order matters
|
||||
function main() {
|
||||
// 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;
|
||||
|
@ -879,11 +1001,11 @@ function main() {
|
|||
}
|
||||
createOutputFiles()
|
||||
generate(
|
||||
files, { target: ts.ScriptTarget.ES5, module: ts.ModuleKind.CommonJS });
|
||||
files, {target: ts.ScriptTarget.ES5, module: ts.ModuleKind.CommonJS});
|
||||
emitHeader(files)
|
||||
emitStructs()
|
||||
emitConsts()
|
||||
emitTypes()
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
||||
main()
|
||||
|
|
|
@ -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()
|
Loading…
Reference in New Issue