Skip to content
Draft
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion src/Program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1027,11 +1027,17 @@ export class Program {
let file = this.getFile(srcPath);
let result: Hover[];
if (file) {
//find the scopes for this file
let scopes = this.getScopesForFile(file);

//if there are no scopes, include the global scope so we at least get the built-in functions
scopes = scopes.length > 0 ? scopes : [this.globalScope];

const event = {
program: this,
file: file,
position: position,
scopes: this.getScopesForFile(file),
scopes: scopes,
hovers: []
} as ProvideHoverEvent;
this.plugins.emit('beforeProvideHover', event);
Expand Down
57 changes: 57 additions & 0 deletions src/bscPlugin/hover/HoverProcessor.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,5 +219,62 @@ describe('HoverProcessor', () => {
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'));
});

it('finds hover for global function', () => {
const file = program.setFile('source/main.brs', `
sub main()
result = Abs(-5)
end sub
`);
program.validate();

// hover over the `Abs` function call
let hover = program.getHover(file.pathAbsolute, util.createPosition(2, 29))[0];
expect(hover).to.exist;
expect(hover.range).to.eql(util.createRange(2, 29, 2, 32));
expect(hover.contents).to.contain('function Abs(x as float) as float');
expect(hover.contents).to.contain('Returns the absolute value of the argument.');
});

it('finds hover for global function when file has no scopes', () => {
// Create a program with no manifest and no scopes to ensure global scope fallback
const testProgram = new Program({ rootDir: rootDir });

const file = testProgram.setFile('standalone/main.brs', `
sub main()
result = Abs(-5)
end sub
`);

// Don't validate - this simulates a file that might not be in any scopes

// hover over the `Abs` function call
let hover = testProgram.getHover(file.pathAbsolute, util.createPosition(2, 29))[0];

testProgram.dispose();

// After the fix, this should now work even when file has no scopes
expect(hover).to.exist;
expect(hover.range).to.eql(util.createRange(2, 29, 2, 32));
expect(hover.contents).to.contain('function Abs(x as float) as float');

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to also see the description of the function. Looking at globalCallables.ts it shows the description for this function should be something like "Returns the absolute value of the argument.".

Here's what I'm expecting.

'a test function
sub test()
end sub

sub init()
    test() 'hover over this, it includes "a test function"
end sub

But for the global functions, it doesn't. It's not even related to files with zero scopes. I can reproduce this in my source/main.brs file.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed by modifying HoverProcessor to show shortDescription and documentation from global callables. The hover now includes descriptions like "Returns the absolute value of the argument." for global functions. Updated tests to verify the descriptions are shown.

expect(hover.contents).to.contain('Returns the absolute value of the argument.');
});

it('finds hover for global function with both shortDescription and documentation', () => {
const file = program.setFile('source/main.brs', `
sub main()
result = Atn(-5)
end sub
`);
program.validate();

// hover over the `Atn` function call
let hover = program.getHover(file.pathAbsolute, util.createPosition(2, 29))[0];
expect(hover).to.exist;
expect(hover.range).to.eql(util.createRange(2, 29, 2, 32));
expect(hover.contents).to.contain('function Atn(x as float) as float');
expect(hover.contents).to.contain('Returns the arctangent (in radians) of the argument.');
expect(hover.contents).to.contain('ATN(X)` returns "the angle whose tangent is X"');
});
});
});
30 changes: 28 additions & 2 deletions src/bscPlugin/hover/HoverProcessor.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { isBrsFile, isFunctionType, isXmlFile } from '../../astUtils/reflection';
import type { BrsFile } from '../../files/BrsFile';
import type { XmlFile } from '../../files/XmlFile';
import type { Hover, ProvideHoverEvent } from '../../interfaces';
import type { Hover, ProvideHoverEvent, Callable } from '../../interfaces';
import type { Token } from '../../lexer/Token';
import { TokenKind } from '../../lexer/TokenKind';
import { BrsTranspileState } from '../../parser/BrsTranspileState';
Expand Down Expand Up @@ -39,6 +39,32 @@
return parts.join('\n');
}

private buildCallableContents(callable: Callable, fence: (code: string) => string) {
const parts = [fence(callable.type.toString())];

Check failure on line 44 in src/bscPlugin/hover/HoverProcessor.ts

View workflow job for this annotation

GitHub Actions / copilot

Trailing spaces not allowed
// For global callables, use shortDescription and documentation properties
if (callable.shortDescription || callable.documentation) {
const docs = [];
if (callable.shortDescription) {
docs.push(callable.shortDescription);
}
if (callable.documentation) {
docs.push(callable.documentation);
}
if (docs.length > 0) {
parts.push('***', docs.join('\n\n'));
}

Check failure on line 56 in src/bscPlugin/hover/HoverProcessor.ts

View workflow job for this annotation

GitHub Actions / copilot

Trailing spaces not allowed
} else {
// For regular callables, use token-based documentation
const tokenDocs = this.getTokenDocumentation((this.event.file as BrsFile).parser.tokens, callable.functionStatement?.func?.functionType);
if (tokenDocs) {
parts.push('***', tokenDocs);
}
}

return parts.join('\n');
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did you have to write the code like this? Aren't the global callabales stored the same way as other functions? I'd rather have the documentation stored on the functions in a unoform way so that user-defined functions and global functions both expose their documentation in the same way. Go find where we expose the global callables and make sure their docs are included properly. Perhaps with comment tokens or something (just a thought, don't do this unless it's already being done this way elsewhere)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're absolutely right. I've refactored the code to use a uniform approach for all callables. Instead of branching logic, I moved getTokenDocumentation to util.ts and modified BrsFile.ts to populate shortDescription when creating user-defined callables from comment tokens. Now HoverProcessor uses the same shortDescription/documentation properties for both global and user-defined functions uniformly. Commit b50df8d.

private getBrsFileHover(file: BrsFile): Hover | undefined {
const scope = this.event.scopes[0];
const fence = (code: string) => util.mdFence(code, 'brightscript');
Expand Down Expand Up @@ -115,7 +141,7 @@
if (callable) {
return {
range: token.range,
contents: this.buildContentsWithDocs(fence(callable.type.toString()), callable.functionStatement?.func?.functionType)
contents: this.buildCallableContents(callable, fence)
};
}
}
Expand Down