diff --git a/langserver/java/body-parser3.js b/langserver/java/body-parser3.js index c845fc4..20c8296 100644 --- a/langserver/java/body-parser3.js +++ b/langserver/java/body-parser3.js @@ -853,7 +853,7 @@ function enumValueList(type, tokens, imports, typemap) { let ctr_args = []; if (tokens.isValue('(')) { if (!tokens.isValue(')')) { - ctr_args = expressionList(tokens, new MethodDeclarations(), type, imports, typemap); + ({ expressions: ctr_args } = expressionList(tokens, new MethodDeclarations(), type, imports, typemap)); tokens.expectValue(')'); } } @@ -1073,7 +1073,7 @@ function forStatement(s, tokens, mdecls, method, imports, typemap) { } // for-updated if (!tokens.isValue(')')) { - s.update = expressionList(tokens, mdecls, method, imports, typemap); + ({ expressions: s.update } = expressionList(tokens, mdecls, method, imports, typemap)); tokens.expectValue(')'); } s.statement = statement(tokens, mdecls, method, imports, typemap); @@ -1574,7 +1574,7 @@ function rootTerm(tokens, mdecls, scope, imports, typemap) { let elements = [], open = tokens.current; tokens.expectValue('{'); if (!tokens.isValue('}')) { - elements = expressionList(tokens, mdecls, scope, imports, typemap, { isArrayLiteral:true }); + ({ expressions: elements } = expressionList(tokens, mdecls, scope, imports, typemap, { isArrayLiteral:true })); tokens.expectValue('}'); } const ident = `{${elements.map(e => e.source).join(',')}}`; @@ -1614,7 +1614,7 @@ function newTerm(tokens, mdecls, scope, imports, typemap) { case '(': tokens.inc(); if (!tokens.isValue(')')) { - ctr_args = expressionList(tokens, mdecls, scope, imports, typemap); + ({ expressions: ctr_args } = expressionList(tokens, mdecls, scope, imports, typemap)); tokens.expectValue(')'); } newtokens = tokens.markEnd(); @@ -1643,9 +1643,12 @@ function newTerm(tokens, mdecls, scope, imports, typemap) { function expressionList(tokens, mdecls, scope, imports, typemap, opts) { let e = expression(tokens, mdecls, scope, imports, typemap); const expressions = [e]; - while (tokens.isValue(',')) { + const commas = []; + while (tokens.current.value === ',') { + commas.push(tokens.consume()); if (opts && opts.isArrayLiteral) { // array literals are allowed a single trailing comma + // @ts-ignore if (tokens.current.value === '}') { break; } @@ -1653,7 +1656,7 @@ function expressionList(tokens, mdecls, scope, imports, typemap, opts) { e = expression(tokens, mdecls, scope, imports, typemap); expressions.push(e); } - return expressions; + return { expressions, commas }; } /** @@ -1771,14 +1774,14 @@ function arrayQualifiers(matches, tokens, mdecls, scope, imports, typemap) { * @param {Map} typemap */ function methodCallQualifier(matches, tokens, mdecls, scope, imports, typemap) { - let args = []; + let args = [], commas = []; tokens.mark(); - tokens.expectValue('('); + const open_bracket = tokens.consume(); if (!tokens.isValue(')')) { - args = expressionList(tokens, mdecls, scope, imports, typemap); + ({ expressions: args, commas } = expressionList(tokens, mdecls, scope, imports, typemap)); tokens.expectValue(')'); } - return new ResolvedIdent(`${matches.source}(${args.map(a => a.source).join(', ')})`, [new MethodCallExpression(matches, args)], [], [], '', [...matches.tokens, ...tokens.markEnd()]); + return new ResolvedIdent(`${matches.source}(${args.map(a => a.source).join(', ')})`, [new MethodCallExpression(matches, open_bracket, args, commas)], [], [], '', [...matches.tokens, ...tokens.markEnd()]); } /** diff --git a/langserver/java/expressiontypes/MethodCallExpression.js b/langserver/java/expressiontypes/MethodCallExpression.js index 1af10c9..d337ca2 100644 --- a/langserver/java/expressiontypes/MethodCallExpression.js +++ b/langserver/java/expressiontypes/MethodCallExpression.js @@ -16,12 +16,16 @@ const { SourceConstructor } = require('../source-types'); class MethodCallExpression extends Expression { /** * @param {ResolvedIdent} instance + * @param {Token} open_bracket * @param {ResolvedIdent[]} args + * @param {Token[]} commas */ - constructor(instance, args) { + constructor(instance, open_bracket, args, commas) { super(); this.instance = instance; + this.open_bracket = open_bracket; this.args = args; + this.commas = commas; } /** @@ -44,14 +48,14 @@ class MethodCallExpression extends Expression { is_ctr = ri.method instanceof SourceConstructor; } if (is_ctr) { - resolveConstructorCall(ri, type.constructors, this.args, () => this.instance.tokens); + resolveConstructorCall(ri, type.constructors, this.open_bracket, this.args, this.commas, () => this.instance.tokens); } else { ri.problems.push(ParseProblem.Error(this.instance.tokens, `'this'/'super' constructor calls can only be used as the first statement of a constructor`)); } return PrimitiveType.map.V; } - return resolveMethodCall(ri, type.methods, this.args, () => this.instance.tokens); + return resolveMethodCall(ri, type.methods, this.open_bracket, this.args, this.commas, () => this.instance.tokens); } tokens() { @@ -62,11 +66,13 @@ class MethodCallExpression extends Expression { /** * @param {ResolveInfo} ri * @param {Method[]} methods + * @param {Token} open_bracket * @param {ResolvedIdent[]} args + * @param {Token[]} commas * @param {() => Token[]} tokens */ -function resolveMethodCall(ri, methods, args, tokens) { - const resolved_args = args.map(arg => arg.resolveExpression(ri)); +function resolveMethodCall(ri, methods, open_bracket, args, commas, tokens) { + const resolved_args = args.map((arg,idx) => arg.resolveExpression(ri)); // all the arguments must be typed expressions, number literals or lambdas /** @type {(JavaType|NumberLiteral|LambdaType|MultiValueType)[]} */ @@ -100,6 +106,32 @@ function resolveMethodCall(ri, methods, args, tokens) { const compatible_methods = reified_methods.filter(m => isCallCompatible(m, arg_types)); const return_types = new Set(compatible_methods.map(m => m.returnType)); + // store the methods and argument position for signature help + const methodIdx = Math.max(reified_methods.indexOf(compatible_methods[0]), 0); + open_bracket.methodCallInfo = { + methods: reified_methods, + methodIdx, + argIdx: 0, + } + args.forEach((arg, idx) => { + const methodCallInfo = { + methods: reified_methods, + methodIdx, + argIdx: idx, + } + // add the info to the previous comma + const c = commas[idx-1]; + if (c) { + c.methodCallInfo = methodCallInfo; + } + // set the info on all the tokens used in the argument + arg.tokens.forEach(tok => { + if (tok.methodCallInfo === null) { + tok.methodCallInfo = methodCallInfo; + } + }) + }) + if (!compatible_methods[0]) { // if any of the arguments is AnyType, just return AnyType if (arg_java_types.find(t => t instanceof AnyType)) { @@ -142,10 +174,12 @@ function resolveMethodCall(ri, methods, args, tokens) { /** * @param {ResolveInfo} ri * @param {Constructor[]} constructors + * @param {Token} open_bracket * @param {ResolvedIdent[]} args + * @param {Token[]} commas * @param {() => Token[]} tokens */ -function resolveConstructorCall(ri, constructors, args, tokens) { +function resolveConstructorCall(ri, constructors, open_bracket, args, commas, tokens) { const resolved_args = args.map(arg => arg.resolveExpression(ri)); // all the arguments must be typed expressions, number literals or lambdas @@ -177,6 +211,32 @@ function resolveConstructorCall(ri, constructors, args, tokens) { // work out which methods are compatible with the call arguments const compatible_ctrs = reifed_ctrs.filter(m => isCallCompatible(m, arg_types)); + // store the methods and argument position for signature help + const methodIdx = reifed_ctrs.indexOf(compatible_ctrs[0]); + open_bracket.methodCallInfo = { + methods: reifed_ctrs, + methodIdx, + argIdx: 0, + } + args.forEach((arg, idx) => { + const methodCallInfo = { + methods: reifed_ctrs, + methodIdx, + argIdx: idx, + } + // add the info to the previous comma + const c = commas[idx-1]; + if (c) { + c.methodCallInfo = methodCallInfo; + } + // set the info on all the tokens used in the argument + arg.tokens.forEach(tok => { + if (tok.methodCallInfo === null) { + tok.methodCallInfo = methodCallInfo; + } + }) + }) + if (!compatible_ctrs[0]) { // if any of the arguments is AnyType, just ignore the call if (arg_java_types.find(t => t instanceof AnyType)) { diff --git a/langserver/java/tokenizer.js b/langserver/java/tokenizer.js index 7df4a67..ade442b 100644 --- a/langserver/java/tokenizer.js +++ b/langserver/java/tokenizer.js @@ -1,3 +1,7 @@ +/** + * @typedef {import('java-mti').Method} Method + * @typedef {import('java-mti').Constructor} Constructor + */ const { TextBlock, BlockRange } = require('./parsetypes/textblock'); /** @@ -51,6 +55,13 @@ class Token extends TextBlock { this.kind = kind; /** @type {{key:string}} */ this.loc = null; + + /** + * Stores information about the resolved methods/constructors this token is an argument for. + * This is used to provide method signature info to vscode + * @type {{methods:(Method|Constructor)[], methodIdx:number, argIdx:number}} + */ + this.methodCallInfo = null; } get value() { diff --git a/langserver/server.js b/langserver/server.js index 38cfda4..24e4409 100644 --- a/langserver/server.js +++ b/langserver/server.js @@ -18,7 +18,7 @@ const { const { TextDocument } = require('vscode-languageserver-textdocument'); -const { loadAndroidLibrary, JavaType, CEIType, ArrayType, PrimitiveType } = require('java-mti'); +const { loadAndroidLibrary, JavaType, CEIType, ArrayType, PrimitiveType, Method } = require('java-mti'); const { ParseProblem } = require('./java/parser'); const { parse } = require('./java/body-parser3'); @@ -231,6 +231,10 @@ connection.onInitialize((params) => { completionProvider: { resolveProvider: true, }, + // Tell the client that the server supports method signature information + signatureHelpProvider : { + triggerCharacters: [ '(' ] + } }, }; }); @@ -758,15 +762,15 @@ connection.onCompletion( return getPackageCompletion(parsed.typemap, options.loc.key.split(':').pop()); } if (/^fqdi:/.test(options.loc.key)) { - // fully-qualified type/field name + // fully-qualified dotted identifier return getFullyQualifiedDottedIdentCompletion(parsed.typemap, options.loc.key.split(':').pop(), { statics: true }); } if (/^fqs:/.test(options.loc.key)) { - // fully-qualified expression + // fully-qualified static expression return getTypedNameCompletion(parsed.typemap, options.loc.key.split(':').pop(), { statics: true }); } if (/^fqi:/.test(options.loc.key)) { - // fully-qualified expression + // fully-qualified instance expression return getTypedNameCompletion(parsed.typemap, options.loc.key.split(':').pop(), { statics: false }); } } @@ -851,30 +855,99 @@ connection.onCompletionResolve( header = `${t.typeKind} **${t.dottedTypeName}**`; } item.detail = detail || ''; - item.documentation = documentation && { - kind: 'markdown', - value: `${header}\n\n${ - documentation - .replace(/(^\/\*+|(?<=\n)[ \t]*\*+\/?|\*+\/)/gm, '') - .replace(/(\n[ \t]*@[a-z]+)|()|(<\/?i>|<\/?em>)|(<\/?b>|<\/?strong>|<\/?dt>)|(<\/?tt>)|(<\/?code>|<\/?pre>|<\/?blockquote>)|(\{@link.+?\}|\{@code.+?\})|(
  • )|(.+?<\/a>)|()|<\/?dd ?.*?>|<\/p ?.*?>|<\/h\d ?.*?>|<\/?div ?.*?>|<\/?[uo]l ?.*?>/gim, (_,prm,p,i,b,tt,c,lc,li,a,h) => { - return prm ? ` ${prm}` - : p ? '\n\n' - : i ? '*' - : b ? '**' - : tt ? '`' - : c ? '\n```' - : lc ? lc.replace(/\{@\w+\s*(.+)\}/, (_,x) => `\`${x.trim()}\``) - : li ? '\n- ' - : a ? a.replace(/.+?\{@docRoot\}(.*?)">(.+?)<\/a>/m, (_,p,t) => `[${t}](https://developer.android.com/${p})`) - : h ? `\n${'#'.repeat(1 + parseInt(h.slice(2,-1),10))} ` - : ''; - }) - }`, - }; + item.documentation = formatDoc(header, documentation); return item; } ); +/** + * @param {string} header + * @param {string} documentation + * @returns {import('vscode-languageserver').MarkupContent} + */ +function formatDoc(header, documentation) { + if (!documentation) { + return null; + } + return { + kind: 'markdown', + value: `${header ? header + '\n\n' : ''}${ + documentation + .replace(/(^\/\*+|(?<=\n)[ \t]*\*+\/?|\*+\/)/gm, '') + .replace(/(\n[ \t]*@[a-z]+)|()|(<\/?i>|<\/?em>)|(<\/?b>|<\/?strong>|<\/?dt>)|(<\/?tt>)|(<\/?code>|<\/?pre>|<\/?blockquote>)|(\{@link.+?\}|\{@code.+?\})|(
  • )|(.+?<\/a>)|()|<\/?dd ?.*?>|<\/p ?.*?>|<\/h\d ?.*?>|<\/?div ?.*?>|<\/?[uo]l ?.*?>/gim, (_,prm,p,i,b,tt,c,lc,li,a,h) => { + return prm ? ` ${prm}` + : p ? '\n\n' + : i ? '*' + : b ? '**' + : tt ? '`' + : c ? '\n```' + : lc ? lc.replace(/\{@\w+\s*(.+)\}/, (_,x) => `\`${x.trim()}\``) + : li ? '\n- ' + : a ? a.replace(/.+?\{@docRoot\}(.*?)">(.+?)<\/a>/m, (_,p,t) => `[${t}](https://developer.android.com/${p})`) + : h ? `\n${'#'.repeat(1 + parseInt(h.slice(2,-1),10))} ` + : ''; + }) + }`, + }; +} + +/** + * @param {import('vscode-languageserver').SignatureHelpParams} request the reeust + */ +async function onSignatureHelp(request) { + /** @type {import('vscode-languageserver').SignatureHelp} */ + let sighelp = { + signatures: [], + activeSignature: 0, + activeParameter: 0, + } + const docinfo = liveParsers.get(request.textDocument.uri); + if (!docinfo || !docinfo.parsed) { + return sighelp; + } + const index = indexAt(request.position, docinfo.content); + const token = docinfo.parsed.unit.getTokenAt(index); + if (!token || !token.methodCallInfo) { + return sighelp; + } + sighelp = { + signatures: token.methodCallInfo.methods.map(m => { + /** @type {import('vscode-languageserver').SignatureInformation} */ + let si = { + label: m.label, + documentation: formatDoc(`#### ${m.owner.simpleTypeName}${m instanceof Method ? `.${m.name}` : ''}()`, m.docs), + parameters: m.parameters.map(p => { + /** @type {import('vscode-languageserver').MarkupContent} */ + let param_documentation = null; + // include a space at the end of the search string so we don't inadvertently match substring parameters, eg: method(type, typeName) + const param_doc_offset = m.docs.indexOf(`@param ${p.name} `); + if (param_doc_offset > 0) { + const doc_match = m.docs.slice(param_doc_offset).match(/@param (\S+)([\d\D]+?)(\n\n|\n[ \t*]*@\w+|$)/); + if (doc_match) { + param_documentation = { + kind:'markdown', + value: `**${doc_match[1]}**: ${formatDoc('', doc_match[2].trim()).value}`, + } + } + } + /** @type {import('vscode-languageserver').ParameterInformation} */ + let pi = { + documentation: param_documentation, + label: p.label + } + return pi; + }) + } + return si; + }), + activeSignature: token.methodCallInfo.methodIdx, + activeParameter: token.methodCallInfo.argIdx, + } + return sighelp; + +} +connection.onSignatureHelp(onSignatureHelp); + /* connection.onDidOpenTextDocument((params) => { // A text document got opened in VS Code.