mirror of
https://github.com/golang/go
synced 2024-11-18 11:04:42 -07:00
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.
|
File diff suppressed because it is too large
Load Diff
472
internal/lsp/protocol/typescript/requests.ts
Normal file
472
internal/lsp/protocol/typescript/requests.ts
Normal file
@ -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
Block a user