Skip to content

Commit

Permalink
Add hover for CONST references. (#648)
Browse files Browse the repository at this point in the history
  • Loading branch information
TwitchBronBron committed Jul 22, 2022
1 parent 5cc5ea3 commit 9b6a405
Show file tree
Hide file tree
Showing 4 changed files with 108 additions and 14 deletions.
2 changes: 1 addition & 1 deletion src/LanguageServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1015,7 +1015,7 @@ export class LanguageServer {
.reduce((set, hover) => {
const hoverContentArray = Array.isArray(hover.contents) ? hover.contents : [hover.contents];
const hoverText = hoverContentArray.map(x => {
return typeof x === 'string' ? x : x.value;
return typeof x === 'string' ? x : x?.value;
}).join('\n');
return set.add(hoverText);
}, new Set<string>()).values()
Expand Down
47 changes: 44 additions & 3 deletions src/bscPlugin/hover/HoverProcessor.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,10 +137,51 @@ describe('HoverProcessor', () => {
`);

let hover = program.getHover(mainFile.pathAbsolute, util.createPosition(2, 25))[0];
expect(hover).to.exist;
expect(hover?.range).to.eql(util.createRange(2, 20, 2, 29));
expect(hover?.contents).to.eql(fence('sub sayMyName(name as string) as void'));
});

expect(hover.range).to.eql(util.createRange(2, 20, 2, 29));
expect(hover.contents).to.eql(fence('sub sayMyName(name as string) as void'));
it('finds top-level constant value', () => {
program.setFile('source/main.bs', `
sub main()
print SOME_VALUE
end sub
const SOME_VALUE = true
`);
// print SOM|E_VALUE
let hover = program.getHover('source/main.bs', util.createPosition(2, 29))[0];
expect(hover?.range).to.eql(util.createRange(2, 26, 2, 36));
expect(hover?.contents).to.eql(fence('const SOME_VALUE = true'));
});

it('finds namespaced constant value', () => {
program.setFile('source/main.bs', `
sub main()
print name.SOME_VALUE
end sub
namespace name
const SOME_VALUE = true
end namespace
`);
// print name.SOM|E_VALUE
let hover = program.getHover('source/main.bs', util.createPosition(2, 34))[0];
expect(hover?.range).to.eql(util.createRange(2, 31, 2, 41));
expect(hover?.contents).to.eql(fence('const name.SOME_VALUE = true'));
});

it('finds deep namespaced constant value', () => {
program.setFile('source/main.bs', `
sub main()
print name.sp.a.c.e.SOME_VALUE
end sub
namespace name.sp.a.c.e
const SOME_VALUE = true
end namespace
`);
// print name.sp.a.c.e.SOM|E_VALUE
let hover = program.getHover('source/main.bs', util.createPosition(2, 43))[0];
expect(hover?.range).to.eql(util.createRange(2, 40, 2, 50));
expect(hover?.contents).to.eql(fence('const name.sp.a.c.e.SOME_VALUE = true'));
});
});
});
48 changes: 38 additions & 10 deletions src/bscPlugin/hover/HoverProcessor.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { SourceNode } from 'source-map';
import type { Hover } from 'vscode-languageserver-types';
import { isBrsFile, isFunctionType, isXmlFile } from '../../astUtils/reflection';
import type { BrsFile } from '../../files/BrsFile';
import type { XmlFile } from '../../files/XmlFile';
import type { Callable, ProvideHoverEvent } from '../../interfaces';
import type { ProvideHoverEvent } from '../../interfaces';
import type { Token } from '../../lexer/Token';
import { TokenKind } from '../../lexer/TokenKind';
import { BrsTranspileState } from '../../parser/BrsTranspileState';
import { ParseMode } from '../../parser/Parser';
import util from '../../util';

export class HoverProcessor {
Expand All @@ -29,7 +32,17 @@ export class HoverProcessor {
}
}

private buildContentsWithDocs(text: string, startingToken: Token) {
const parts = [text];
const docs = this.getTokenDocumentation((this.event.file as BrsFile).parser.tokens, startingToken);
if (docs) {
parts.push('***', docs);
}
return parts.join('\n');
}

private getBrsFileHover(file: BrsFile): Hover {
const scope = this.event.scopes[0];
const fence = (code: string) => util.mdFence(code, 'brightscript');
//get the token at the position
let token = file.getTokenAt(this.event.position);
Expand All @@ -47,6 +60,22 @@ export class HoverProcessor {
return null;
}

const expression = file.getClosestExpression(this.event.position);
if (expression) {
let containingNamespace = file.getNamespaceStatementForPosition(expression.range.start)?.getName(ParseMode.BrighterScript);
const fullName = util.getAllDottedGetParts(expression)?.map(x => x.text).join('.');

//find a constant with this name
const constant = scope.getConstFileLink(fullName, containingNamespace);
if (constant) {
const constantValue = new SourceNode(null, null, null, constant.item.value.transpile(new BrsTranspileState(file))).toString();
return {
contents: this.buildContentsWithDocs(fence(`const ${constant.item.fullName} = ${constantValue}`), constant.item.tokens.const),
range: token.range
};
}
}

let lowerTokenText = token.text.toLowerCase();

//look through local variables first
Expand Down Expand Up @@ -88,19 +117,21 @@ export class HoverProcessor {
if (callable) {
return {
range: token.range,
contents: this.getCallableDocumentation(callable)
contents: this.buildContentsWithDocs(fence(callable.type.toString()), callable.functionStatement?.func?.functionType)
};
}
}
}

/**
* Build a hover documentation for a callable.
* Combine all the documentation found before a token (i.e. comment tokens)
*/
private getCallableDocumentation(callable: Callable) {
private getTokenDocumentation(tokens: Token[], token?: Token) {
const comments = [] as Token[];
const tokens = callable.file.parser.tokens as Token[];
const idx = tokens.indexOf(callable.functionStatement?.func.functionType);
const idx = tokens?.indexOf(token);
if (!idx || idx === -1) {
return undefined;
}
for (let i = idx - 1; i >= 0; i--) {
const token = tokens[i];
//skip whitespace and newline chars
Expand All @@ -115,14 +146,11 @@ export class HoverProcessor {
break;
}
}
let result = util.mdFence(callable.type.toString(), 'brightscript');
if (comments.length > 0) {
result += '\n***\n' + comments.reverse().map(x => x.text.replace(/^('|rem)/i, '')).join('\n');
return comments.reverse().map(x => x.text.replace(/^('|rem)/i, '')).join('\n');
}
return result;
}


private getXmlFileHover(file: XmlFile) {
//TODO add xml hovers
return undefined;
Expand Down
25 changes: 25 additions & 0 deletions src/files/BrsFile.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { CodeWithSourceMap } from 'source-map';
import { SourceNode } from 'source-map';
import type { CompletionItem, Position, Location, Diagnostic } from 'vscode-languageserver';
import { CancellationTokenSource } from 'vscode-languageserver';
import { CompletionItemKind, SymbolKind, SignatureInformation, ParameterInformation, DocumentSymbol, SymbolInformation, TextEdit } from 'vscode-languageserver';
import chalk from 'chalk';
import * as path from 'path';
Expand Down Expand Up @@ -182,6 +183,30 @@ export class BrsFile {
}
}

/**
* Walk the AST and find the expression that this token is most specifically contained within
*/
public getClosestExpression(position: Position) {
const handle = new CancellationTokenSource();
let containingNode: Expression | Statement;
this.ast.walk((node) => {
const latestContainer = containingNode;
//bsc walks depth-first
if (util.rangeContains(node.range, position)) {
containingNode = node;
}
//we had a match before, and don't now. this means we've finished walking down the whole way, and found our match
if (latestContainer && !containingNode) {
containingNode = latestContainer;
handle.cancel();
}
}, {
walkMode: WalkMode.visitAllRecursive,
cancel: handle.token
});
return containingNode;
}

public get parser() {
if (!this._parser) {
//remove the typedef file (if it exists)
Expand Down

0 comments on commit 9b6a405

Please sign in to comment.