Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
5 changes: 5 additions & 0 deletions src/LanguageServer.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,11 @@ describe('LanguageServer', () => {
onWillSaveTextDocumentWaitUntil: () => null,
onDidSaveTextDocument: () => null,
onRequest: () => null,
languages: {
inlayHint: {
on: () => null
}
},
workspace: {
getWorkspaceFolders: () => {
return workspaceFolders.map(
Expand Down
17 changes: 16 additions & 1 deletion src/LanguageServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ import type {
SelectionRangeParams,
RenameFilesParams,
WorkspaceEdit,
TextEdit
TextEdit,
InlayHint,
InlayHintParams
} from 'vscode-languageserver/node';
import {
SemanticTokensRequest,
Expand Down Expand Up @@ -177,6 +179,9 @@ export class LanguageServer {
//Register semantic token requests. TODO switch to a more specific connection function call once they actually add it
this.connection.onRequest(SemanticTokensRequest.method, this.onFullSemanticTokens.bind(this));

//Register inlay hint requests. Inlay hints live under connection.languages and aren't picked up by the on* auto-bind loop above
this.connection.languages.inlayHint.on(this.onInlayHint.bind(this));

//file-operation requests live under connection.workspace, so they aren't picked up by the on* auto-bind loop above
this.connection.workspace.onWillRenameFiles(this.onWillRenameFiles.bind(this));

Expand Down Expand Up @@ -239,6 +244,7 @@ export class LanguageServer {
definitionProvider: true,
hoverProvider: true,
selectionRangeProvider: true,
inlayHintProvider: true,
executeCommandProvider: {
commands: [
CustomCommands.TranspileFile
Expand Down Expand Up @@ -631,6 +637,15 @@ export class LanguageServer {
return this.projectManager.getSelectionRanges({ srcPath: srcPath, positions: params.positions });
}

@AddStackToErrorMessage
public async onInlayHint(params: InlayHintParams): Promise<InlayHint[]> {
this.logger.debug('onInlayHint', params);

const srcPath = util.uriToPath(params.textDocument.uri);
const result = await this.projectManager.getInlayHints({ srcPath: srcPath, range: params.range });
return result ?? [];
}

@AddStackToErrorMessage
public async onDocumentSymbol(params: DocumentSymbolParams) {
this.logger.debug('onDocumentSymbol', params);
Expand Down
25 changes: 23 additions & 2 deletions src/Program.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as assert from 'assert';
import * as fsExtra from 'fs-extra';
import * as path from 'path';
import type { CodeAction, CompletionItem, Position, Range, SignatureInformation, Location, DocumentSymbol, CancellationToken, SelectionRange } from 'vscode-languageserver';
import type { CodeAction, CompletionItem, Position, Range, SignatureInformation, Location, DocumentSymbol, CancellationToken, SelectionRange, InlayHint } from 'vscode-languageserver';
import { CancellationTokenSource, CompletionItemKind } from 'vscode-languageserver';
import type { BsConfig, FinalizedBsConfig } from './BsConfig';
import { Scope } from './Scope';
Expand All @@ -10,7 +10,7 @@ import { SymbolTable } from './SymbolTable';
import { DiagnosticMessages } from './DiagnosticMessages';
import { BrsFile } from './files/BrsFile';
import { XmlFile } from './files/XmlFile';
import type { BsDiagnostic, File, FileReference, FileObj, BscFile, SemanticToken, AfterFileTranspileEvent, FileLink, ProvideHoverEvent, ProvideCompletionsEvent, Hover, ProvideDefinitionEvent, ProvideReferencesEvent, ProvideDocumentSymbolsEvent, ProvideWorkspaceSymbolsEvent, ProvideSelectionRangesEvent, OnGetSourceFixAllCodeActionsEvent } from './interfaces';
import type { BsDiagnostic, File, FileReference, FileObj, BscFile, SemanticToken, AfterFileTranspileEvent, FileLink, ProvideHoverEvent, ProvideCompletionsEvent, Hover, ProvideDefinitionEvent, ProvideReferencesEvent, ProvideDocumentSymbolsEvent, ProvideWorkspaceSymbolsEvent, ProvideSelectionRangesEvent, ProvideInlayHintsEvent, OnGetSourceFixAllCodeActionsEvent } from './interfaces';
import type { SourceFixAllCodeAction } from './CodeActionUtil';
import { codeActionUtil } from './CodeActionUtil';
import { standardizePath as s, util } from './util';
Expand Down Expand Up @@ -1233,6 +1233,27 @@ export class Program {
return [];
}

/**
* Get inlay hints for the given file and range.
*/
public getInlayHints(srcPath: string, range: Range): InlayHint[] {
const file = this.getFile(srcPath);
if (file) {
const event: ProvideInlayHintsEvent = {
program: this,
file: file,
range: range,
scopes: this.getScopesForFile(file),
inlayHints: []
};
this.plugins.emit('beforeProvideInlayHints', event);
this.plugins.emit('provideInlayHints', event);
this.plugins.emit('afterProvideInlayHints', event);
return event.inlayHints;
}
return [];
}

/**
* Compute code actions for the given file and range
*/
Expand Down
7 changes: 6 additions & 1 deletion src/bscPlugin/BscPlugin.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { isBrsFile, isXmlFile } from '../astUtils/reflection';
import type { BeforeFileTranspileEvent, Plugin, OnFileValidateEvent, OnGetCodeActionsEvent, OnGetSourceFixAllCodeActionsEvent, ProvideHoverEvent, OnGetSemanticTokensEvent, OnScopeValidateEvent, ProvideCompletionsEvent, ProvideDefinitionEvent, ProvideReferencesEvent, ProvideDocumentSymbolsEvent, ProvideWorkspaceSymbolsEvent, ProvideSelectionRangesEvent } from '../interfaces';
import type { BeforeFileTranspileEvent, Plugin, OnFileValidateEvent, OnGetCodeActionsEvent, OnGetSourceFixAllCodeActionsEvent, ProvideHoverEvent, OnGetSemanticTokensEvent, OnScopeValidateEvent, ProvideCompletionsEvent, ProvideDefinitionEvent, ProvideReferencesEvent, ProvideDocumentSymbolsEvent, ProvideWorkspaceSymbolsEvent, ProvideSelectionRangesEvent, ProvideInlayHintsEvent } from '../interfaces';
import type { Program } from '../Program';
import { CodeActionsProcessor } from './codeActions/CodeActionsProcessor';
import { FixAllCodeActionsProcessor } from './codeActions/FixAllCodeActionsProcessor';
Expand All @@ -16,6 +16,7 @@ import { ScopeValidator } from './validation/ScopeValidator';
import { XmlFileValidator } from './validation/XmlFileValidator';
import { WorkspaceSymbolProcessor } from './symbols/WorkspaceSymbolProcessor';
import { SelectionRangesProcessor } from './selectionRanges/SelectionRangesProcessor';
import { InlayHintProcessor } from './inlayHints/InlayHintProcessor';

export class BscPlugin implements Plugin {
public name = 'BscPlugin';
Expand Down Expand Up @@ -56,6 +57,10 @@ export class BscPlugin implements Plugin {
new SelectionRangesProcessor(event).process();
}

public provideInlayHints(event: ProvideInlayHintsEvent) {
new InlayHintProcessor(event).process();
}

public onGetSemanticTokens(event: OnGetSemanticTokensEvent) {
if (isBrsFile(event.file)) {
return new BrsFileSemanticTokensProcessor(event as any).process();
Expand Down
162 changes: 162 additions & 0 deletions src/bscPlugin/inlayHints/InlayHintProcessor.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import { expect } from '../../chai-config.spec';
import { Program } from '../../Program';
import { util } from '../../util';
import { rootDir, trim } from '../../testHelpers.spec';
import type { InlayHint } from 'vscode-languageserver-types';
import { InlayHintKind } from 'vscode-languageserver-types';

describe('InlayHintProcessor', () => {
let program: Program;

beforeEach(() => {
program = new Program({ rootDir: rootDir });
});

afterEach(() => {
program.dispose();
});

function getInlayHints(filePath: string, code: string): InlayHint[] {
const file = program.setFile(filePath, code);
program.validate();
const lines = code.split('\n');
const range = util.createRange(0, 0, lines.length, 0);
return program.getInlayHints(file.pathAbsolute, range);
}

it('returns empty array when no calls exist', () => {
const hints = getInlayHints('source/main.bs', trim`
sub main()
x = 1
end sub
`);
expect(hints).to.eql([]);
});

it('emits parameter name hints for a regular function call', () => {
const hints = getInlayHints('source/main.bs', trim`
sub main()
sayHello("world", 42)
end sub
sub sayHello(name as string, age as integer)
end sub
`);
expect(hints.map(h => h.label)).to.eql(['name:', 'age:']);
expect(hints.every(h => h.kind === InlayHintKind.Parameter)).to.be.true;
expect(hints.every(h => h.paddingRight === true)).to.be.true;
});

it('positions hints at the start of each argument', () => {
const hints = getInlayHints('source/main.bs', trim`
sub main()
sayHello("world", 42)
end sub
sub sayHello(name as string, age as integer)
end sub
`);
expect(hints[0].position).to.eql(util.createPosition(1, 13));
//"world" is 7 chars, so 42 starts at column 13 + 7 + 2 = 22
expect(hints[1].position).to.eql(util.createPosition(1, 22));
});

it('skips hints when the argument is a variable matching the parameter name', () => {
const hints = getInlayHints('source/main.bs', trim`
sub main()
name = "world"
sayHello(name, 42)
end sub
sub sayHello(name as string, age as integer)
end sub
`);
//first arg matches param name and should be suppressed; second arg should still emit
expect(hints.map(h => h.label)).to.eql(['age:']);
});

it('limits hints to the requested range', () => {
const code = trim`
sub main()
sayHello("world", 42)
sayHello("again", 99)
end sub
sub sayHello(name as string, age as integer)
end sub
`;
const file = program.setFile('source/main.bs', code);
program.validate();
//range only covers line 1 (the first call)
const range = util.createRange(1, 0, 1, 100);
const hints = program.getInlayHints(file.pathAbsolute, range);
expect(hints.map(h => h.label)).to.eql(['name:', 'age:']);
expect(hints.every(h => h.position.line === 1)).to.be.true;
});

it('emits hints for namespace calls', () => {
const hints = getInlayHints('source/main.bs', trim`
sub main()
math.add(1, 2)
end sub
namespace math
sub add(left as integer, right as integer)
end sub
end namespace
`);
expect(hints.map(h => h.label)).to.eql(['left:', 'right:']);
});

it('emits hints for class constructor calls', () => {
const hints = getInlayHints('source/main.bs', trim`
sub main()
p = new Person("Alice", 30)
end sub
class Person
sub new(name as string, age as integer)
end sub
end class
`);
expect(hints.map(h => h.label)).to.eql(['name:', 'age:']);
});

it('emits hints for m.method() calls inside a class', () => {
const hints = getInlayHints('source/main.bs', trim`
class Greeter
sub greet()
m.sayHello("world", 42)
end sub
sub sayHello(name as string, age as integer)
end sub
end class
`);
expect(hints.map(h => h.label)).to.eql(['name:', 'age:']);
});

it('does not emit hints when the function is unknown', () => {
const hints = getInlayHints('source/main.bs', trim`
sub main()
unknownFunction(1, 2)
end sub
`);
expect(hints).to.eql([]);
});

it('does not emit hints for excess arguments past the parameter list', () => {
const hints = getInlayHints('source/main.bs', trim`
sub main()
takesOne(1, 2, 3)
end sub
sub takesOne(only as integer)
end sub
`);
expect(hints.map(h => h.label)).to.eql(['only:']);
});

it('does not crash on calls with no arguments', () => {
const hints = getInlayHints('source/main.bs', trim`
sub main()
noArgs()
end sub
sub noArgs()
end sub
`);
expect(hints).to.eql([]);
});
});
Loading
Loading