Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Plugin support for hover #393

Merged
merged 11 commits into from
Jul 21, 2022
17 changes: 1 addition & 16 deletions src/LanguageServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import type {
CompletionItem,
Connection,
DidChangeWatchedFilesParams,
Hover,
InitializeParams,
ServerCapabilities,
TextDocumentPositionParams,
Expand Down Expand Up @@ -874,24 +873,10 @@ export class LanguageServer {

let pathAbsolute = util.uriToPath(params.textDocument.uri);
let workspaces = this.getWorkspaces();
let hovers = await Promise.all(
Array.prototype.concat.call([],
workspaces.map(async (x) => x.builder.program.getHover(pathAbsolute, params.position))
)
) as Hover[];
let hovers = workspaces.map((x) => x.builder.program.getHover(pathAbsolute, params.position));

//return the first non-falsey hover. TODO is there a way to handle multiple hover results?
let hover = hovers.filter((x) => !!x)[0];

//TODO improve this to support more than just .brs files
if (hover?.contents) {
//create fenced code block to get colorization
hover.contents = {
//TODO - make the program.getHover call figure out what language this is for
language: 'brightscript',
value: hover.contents as string
};
}
return hover;
}

Expand Down
9 changes: 7 additions & 2 deletions src/PluginInterface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,14 @@ export default class PluginInterface<T extends CompilerPlugin = CompilerPlugin>
for (let plugin of this.plugins) {
if ((plugin as any)[event]) {
try {
this.logger.time(LogLevel.debug, [plugin.name, event], () => {
(plugin as any)[event](...args);
const returnValue = this.logger.time(LogLevel.debug, [plugin.name, event], () => {
return (plugin as any)[event](...args);
});

//plugins can short-circuit the event by returning `false`
if (returnValue === false) {
TwitchBronBron marked this conversation as resolved.
Show resolved Hide resolved
return;
}
} catch (err) {
this.logger.error(`Error when calling plugin ${plugin.name}.${event}:`, err);
}
Expand Down
24 changes: 16 additions & 8 deletions src/Program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { Scope } from './Scope';
import { DiagnosticMessages } from './DiagnosticMessages';
import { BrsFile } from './files/BrsFile';
import { XmlFile } from './files/XmlFile';
import type { BsDiagnostic, File, FileReference, FileObj, BscFile } from './interfaces';
import type { BsDiagnostic, File, FileReference, FileObj, BscFile, OnGetHoverEvent } from './interfaces';
import { standardizePath as s, util } from './util';
import { XmlScope } from './XmlScope';
import { DiagnosticFilterer } from './DiagnosticFilterer';
Expand Down Expand Up @@ -802,14 +802,22 @@ export class Program {
}
}

public getHover(pathAbsolute: string, position: Position) {
//find the file
let file = this.getFile(pathAbsolute);
if (!file) {
return null;
/**
* Get hover information for a file and position
*/
public getHover(srcPath: string, position: Position) {
let file = this.getFile(srcPath);
if (file) {
const event = {
program: this,
file: file,
position: position,
scopes: this.getScopesForFile(file),
hover: null
} as OnGetHoverEvent;
this.plugins.emit('onGetHover', event);
return event.hover;
}

return Promise.resolve(file.getHover(position));
}

/**
Expand Down
7 changes: 6 additions & 1 deletion src/bscPlugin/BscPlugin.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import type { OnGetCodeActionsEvent, CompilerPlugin } from '../interfaces';
import type { OnGetCodeActionsEvent, CompilerPlugin, OnGetHoverEvent } from '../interfaces';
import { CodeActionsProcessor } from './codeActions/CodeActionsProcessor';
import { HoverProcessor } from './hover/HoverProcessor';

export class BscPlugin implements CompilerPlugin {
public name = 'BscPlugin';

public onGetCodeActions(event: OnGetCodeActionsEvent) {
new CodeActionsProcessor(event).process();
}

public onGetHover(event: OnGetHoverEvent) {
return new HoverProcessor(event).process();
}
}
157 changes: 157 additions & 0 deletions src/bscPlugin/hover/HoverProcessor.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import { expect } from 'chai';
import { Program } from '../../Program';
import util, { standardizePath as s } from '../../util';
import { createSandbox } from 'sinon';
let sinon = createSandbox();

let rootDir = s`${process.cwd()}/.tmp/rootDir`;

describe('HoverProcessor', () => {
let program: Program;
beforeEach(() => {
program = new Program({ rootDir: rootDir, sourceMap: true });
});
afterEach(() => {
sinon.restore();
program.dispose();
});

it('does not short-circuit the event since our plugin is the base plugin', () => {
const mock = sinon.mock();
program.plugins.add({
name: 'test-plugin',
onGetHover: mock
});
const file = program.addOrReplaceFile('source/main.brs', `
sub main()
end sub
`);
//get the hover
program.getHover(file.pathAbsolute, util.createPosition(1, 20));
//the onGetHover function from `test-plugin` should always get called because
//BscPlugin should never short-circuit the event
expect(mock.called).to.be.true;
});

describe('BrsFile', () => {
it('works for param types', () => {
const file = program.addOrReplaceFile('source/main.brs', `
sub DoSomething(name as string)
name = 1
sayMyName = function(name as string)
end function
end sub
`);

//hover over the `name = 1` line
let hover = program.getHover(file.pathAbsolute, util.createPosition(2, 24));
expect(hover).to.exist;
expect(hover.range).to.eql(util.createRange(2, 20, 2, 24));

//hover over the `name` parameter declaration
hover = program.getHover(file.pathAbsolute, util.createPosition(1, 34));
expect(hover).to.exist;
expect(hover.range).to.eql(util.createRange(1, 32, 1, 36));
});

//ignore this for now...it's not a huge deal
it('does not match on keywords or data types', () => {
let file = program.addOrReplaceFile({ src: `${rootDir}/source/main.brs`, dest: 'source/main.brs' }, `
sub Main(name as string)
end sub
sub as()
end sub
`);
//hover over the `as`
expect(program.getHover(file.pathAbsolute, util.createPosition(1, 31))).not.to.exist;
//hover over the `string`
expect(program.getHover(file.pathAbsolute, util.createPosition(1, 36))).not.to.exist;
});

it('finds declared function', () => {
let file = program.addOrReplaceFile({ src: `${rootDir}/source/main.brs`, dest: 'source/main.brs' }, `
function Main(count = 1)
firstName = "bob"
age = 21
shoeSize = 10
end function
`);

let hover = program.getHover(file.pathAbsolute, util.createPosition(1, 28));
expect(hover).to.exist;

expect(hover.range).to.eql(util.createRange(1, 25, 1, 29));
expect(hover.contents).to.eql({
language: 'brighterscript',
value: 'function Main(count? as dynamic) as dynamic'
});
});

it('finds variable function hover in same scope', () => {
let file = program.addOrReplaceFile({ src: `${rootDir}/source/main.brs`, dest: 'source/main.brs' }, `
sub Main()
sayMyName = sub(name as string)
end sub

sayMyName()
end sub
`);

let hover = program.getHover(file.pathAbsolute, util.createPosition(5, 24));

expect(hover.range).to.eql(util.createRange(5, 20, 5, 29));
expect(hover.contents).to.eql({
language: 'brighterscript',
value: 'sub sayMyName(name as string) as void'
});
});

it('finds function hover in file scope', () => {
let file = program.addOrReplaceFile({ src: `${rootDir}/source/main.brs`, dest: 'source/main.brs' }, `
sub Main()
sayMyName()
end sub

sub sayMyName()

end sub
`);

let hover = program.getHover(file.pathAbsolute, util.createPosition(2, 25));

expect(hover.range).to.eql(util.createRange(2, 20, 2, 29));
expect(hover.contents).to.eql({
language: 'brighterscript',
value: 'sub sayMyName() as void'
});
});

it('finds function hover in scope', () => {
let rootDir = process.cwd();
program = new Program({
rootDir: rootDir
});

let mainFile = program.addOrReplaceFile({ src: `${rootDir}/source/main.brs`, dest: 'source/main.brs' }, `
sub Main()
sayMyName()
end sub
`);

program.addOrReplaceFile({ src: `${rootDir}/source/lib.brs`, dest: 'source/lib.brs' }, `
sub sayMyName(name as string)

end sub
`);

let hover = program.getHover(mainFile.pathAbsolute, util.createPosition(2, 25));
expect(hover).to.exist;

expect(hover.range).to.eql(util.createRange(2, 20, 2, 29));
expect(hover.contents).to.eql({
language: 'brighterscript',
value: 'sub sayMyName(name as string) as void'
});
});
});
});
106 changes: 106 additions & 0 deletions src/bscPlugin/hover/HoverProcessor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import type { Hover } from 'vscode-languageserver-types';
import { isBrsFile, isFunctionType, isXmlFile } from '../../astUtils';
import type { BrsFile } from '../../files/BrsFile';
import type { XmlFile } from '../../files/XmlFile';
import type { OnGetHoverEvent } from '../../interfaces';
import { TokenKind } from '../../lexer/TokenKind';

export class HoverProcessor {
public constructor(
public event: OnGetHoverEvent
) {

}

public process() {
let hover: Hover;
if (isBrsFile(this.event.file)) {
hover = this.getBrsFileHover(this.event.file);
} else if (isXmlFile(this.event.file)) {
hover = this.getXmlFileHover(this.event.file);
}

//if we got a result, "return" it
if (hover) {
//assign the hover to the event
this.event.hover = hover;
}
}

private getBrsFileHover(file: BrsFile): Hover {
//get the token at the position
let token = file.getTokenAt(this.event.position);

let hoverTokenTypes = [
TokenKind.Identifier,
TokenKind.Function,
TokenKind.EndFunction,
TokenKind.Sub,
TokenKind.EndSub
];

//throw out invalid tokens and the wrong kind of tokens
if (!token || !hoverTokenTypes.includes(token.kind)) {
return null;
}

let lowerTokenText = token.text.toLowerCase();

//look through local variables first
{
//get the function scope for this position (if exists)
let functionScope = file.getFunctionScopeAtPosition(this.event.position);
if (functionScope) {
//find any variable with this name
for (const varDeclaration of functionScope.variableDeclarations) {
//we found a variable declaration with this token text!
if (varDeclaration.name.toLowerCase() === lowerTokenText) {
let typeText: string;
if (isFunctionType(varDeclaration.type)) {
typeText = varDeclaration.type.toString();
} else {
typeText = `${varDeclaration.name} as ${varDeclaration.type.toString()}`;
}
return {
range: token.range,
contents: {
language: 'brighterscript',
value: typeText
}
};
}
}
for (const labelStatement of functionScope.labelStatements) {
if (labelStatement.name.toLocaleLowerCase() === lowerTokenText) {
return {
range: token.range,
contents: {
language: 'brighterscript',
value: `${labelStatement.name}: label`
}
};
}
}
}
}

//look through all callables in relevant scopes
for (let scope of this.event.scopes) {
let callable = scope.getCallableByName(lowerTokenText);
if (callable) {
return {
range: token.range,
contents: {
language: 'brighterscript',
value: callable.type.toString()
}
};
}
}
}

private getXmlFileHover(file: XmlFile) {
//TODO add xml hovers
return undefined;
}
}
Loading