diff --git a/docs/plugins.md b/docs/plugins.md index 3a1d90a44..470e574e0 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -108,6 +108,16 @@ After file addition/removal (note: throttled/debounced): Code Actions - `onGetCodeActions` +Completions + - `beforeProvideCompletions` + - `provideCompletions` + - `afterProvideCompletions` + +Hovers + - `beforeProvideHover` + - `provideHover` + - `afterProvideHover` + Semantic Tokens - `onGetSemanticTokens` @@ -149,6 +159,33 @@ export interface CompilerPlugin { beforeProgramTranspile?: (program: Program, entries: TranspileObj[], editor: AstEditor) => void; afterProgramTranspile?: (program: Program, entries: TranspileObj[], editor: AstEditor) => void; onGetCodeActions?: PluginHandler; + + /** + * Emitted before the program starts collecting completions + */ + beforeProvideCompletions?: PluginHandler; + /** + * Use this event to contribute completions + */ + provideCompletions?: PluginHandler; + /** + * Emitted after the program has finished collecting completions, but before they are sent to the client + */ + afterProvideCompletions?: PluginHandler; + + /** + * Called before the `provideHover` hook. Use this if you need to prepare any of the in-memory objects before the `provideHover` gets called + */ + beforeProvideHover?: PluginHandler; + /** + * Called when bsc looks for hover information. Use this if your plugin wants to contribute hover information. + */ + provideHover?: PluginHandler; + /** + * Called after the `provideHover` hook. Use this if you want to intercept or sanitize the hover data (even from other plugins) before it gets sent to the client. + */ + afterProvideHover?: PluginHandler; + onGetSemanticTokens?: PluginHandler; //scope events afterScopeCreate?: (scope: Scope) => void; diff --git a/package-lock.json b/package-lock.json index e38a2f4ae..4f6b4f309 100644 --- a/package-lock.json +++ b/package-lock.json @@ -529,7 +529,7 @@ "@types/json5": { "version": "0.0.29", "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", - "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=", "dev": true }, "@types/marked": { @@ -3367,7 +3367,7 @@ "jsonfile": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", "requires": { "graceful-fs": "^4.1.6" } diff --git a/src/Program.spec.ts b/src/Program.spec.ts index b1b1407aa..3bfd6d694 100644 --- a/src/Program.spec.ts +++ b/src/Program.spec.ts @@ -1542,7 +1542,7 @@ describe('Program', () => { describe('getCompletions', () => { it('returns all functions in scope', () => { - program.setFile({ src: `${rootDir}/source/main.brs`, dest: 'source/main.brs' }, ` + program.setFile('source/main.brs', ` sub Main() end sub @@ -1559,7 +1559,7 @@ describe('Program', () => { let completions = program //get completions - .getCompletions(`${rootDir}/source/main.brs`, Position.create(2, 10)) + .getCompletions(`${rootDir}/source/main.brs`, util.createPosition(2, 10)) //only keep the label property for this test .map(x => pick(x, 'label')); @@ -1569,7 +1569,7 @@ describe('Program', () => { }); it('returns all variables in scope', () => { - program.setFile({ src: `${rootDir}/source/main.brs`, dest: 'source/main.brs' }, ` + program.setFile('source/main.brs', ` sub Main() name = "bob" age = 20 @@ -1585,7 +1585,7 @@ describe('Program', () => { program.validate(); - let completions = program.getCompletions(`${rootDir}/source/main.brs`, Position.create(2, 10)); + let completions = program.getCompletions(`${rootDir}/source/main.brs`, util.createPosition(2, 10)); let labels = completions.map(x => pick(x, 'label')); expect(labels).to.deep.include({ label: 'Main' }); @@ -1608,7 +1608,7 @@ describe('Program', () => { }); it('finds parameters', () => { - program.setFile({ src: `${rootDir}/source/main.brs`, dest: 'source/main.brs' }, ` + program.setFile('source/main.brs', ` sub Main(count = 1) firstName = "bob" age = 21 diff --git a/src/Program.ts b/src/Program.ts index 4a7f29358..3b6721e58 100644 --- a/src/Program.ts +++ b/src/Program.ts @@ -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, SemanticToken, AfterFileTranspileEvent, FileLink, ProvideHoverEvent } from './interfaces'; +import type { BsDiagnostic, File, FileReference, FileObj, BscFile, SemanticToken, AfterFileTranspileEvent, FileLink, ProvideHoverEvent, ProvideCompletionsEvent } from './interfaces'; import { standardizePath as s, util } from './util'; import { XmlScope } from './XmlScope'; import { DiagnosticFilterer } from './DiagnosticFilterer'; @@ -836,20 +836,6 @@ export class Program { if (!file) { return []; } - let result = [] as CompletionItem[]; - - if (isBrsFile(file) && file.isPositionNextToTokenKind(position, TokenKind.Callfunc)) { - // is next to a @. callfunc invocation - must be an interface method - for (const scope of this.getScopes().filter((s) => isXmlScope(s))) { - let fileLinks = this.getStatementsForXmlFile(scope as XmlScope); - for (let fileLink of fileLinks) { - - result.push(scope.createCompletionFromFunctionStatement(fileLink.item)); - } - } - //no other result is possible in this case - return result; - } //find the scopes for this file let scopes = this.getScopesForFile(file); @@ -857,22 +843,21 @@ export class Program { //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]; - //get the completions from all scopes for this file - let allCompletions = util.flatMap( - scopes.map(ctx => file.getCompletions(position, ctx)), - c => c - ); + const event: ProvideCompletionsEvent = { + program: this, + file: file, + scopes: scopes, + position: position, + completions: [] + }; - //only keep completions common to every scope for this file - let keyCounts = {} as Record; - for (let completion of allCompletions) { - let key = `${completion.label}-${completion.kind}`; - keyCounts[key] = keyCounts[key] ? keyCounts[key] + 1 : 1; - if (keyCounts[key] === scopes.length) { - result.push(completion); - } - } - return result; + this.plugins.emit('beforeProvideCompletions', event); + + this.plugins.emit('provideCompletions', event); + + this.plugins.emit('afterProvideCompletions', event); + + return event.completions; } /** diff --git a/src/bscPlugin/BscPlugin.ts b/src/bscPlugin/BscPlugin.ts index 3d6a75976..e4950d95c 100644 --- a/src/bscPlugin/BscPlugin.ts +++ b/src/bscPlugin/BscPlugin.ts @@ -1,8 +1,8 @@ import { isBrsFile } from '../astUtils/reflection'; -import type { BrsFile } from '../files/BrsFile'; -import type { BeforeFileTranspileEvent, CompilerPlugin, OnFileValidateEvent, OnGetCodeActionsEvent, ProvideHoverEvent, OnGetSemanticTokensEvent, OnScopeValidateEvent } from '../interfaces'; +import type { BeforeFileTranspileEvent, CompilerPlugin, OnFileValidateEvent, OnGetCodeActionsEvent, ProvideHoverEvent, OnGetSemanticTokensEvent, OnScopeValidateEvent, ProvideCompletionsEvent } from '../interfaces'; import type { Program } from '../Program'; import { CodeActionsProcessor } from './codeActions/CodeActionsProcessor'; +import { CompletionsProcessor } from './completions/CompletionsProcessor'; import { HoverProcessor } from './hover/HoverProcessor'; import { BrsFileSemanticTokensProcessor } from './semanticTokens/BrsFileSemanticTokensProcessor'; import { BrsFilePreTranspileProcessor } from './transpile/BrsFilePreTranspileProcessor'; @@ -20,6 +20,10 @@ export class BscPlugin implements CompilerPlugin { return new HoverProcessor(event).process(); } + public provideCompletions(event: ProvideCompletionsEvent) { + new CompletionsProcessor(event).process(); + } + public onGetSemanticTokens(event: OnGetSemanticTokensEvent) { if (isBrsFile(event.file)) { return new BrsFileSemanticTokensProcessor(event as any).process(); @@ -28,7 +32,7 @@ export class BscPlugin implements CompilerPlugin { public onFileValidate(event: OnFileValidateEvent) { if (isBrsFile(event.file)) { - return new BrsFileValidator(event as OnFileValidateEvent).process(); + return new BrsFileValidator(event as any).process(); } } diff --git a/src/bscPlugin/completions/CompletionsProcessor.ts b/src/bscPlugin/completions/CompletionsProcessor.ts new file mode 100644 index 000000000..7cadffc37 --- /dev/null +++ b/src/bscPlugin/completions/CompletionsProcessor.ts @@ -0,0 +1,53 @@ +import { isBrsFile, isXmlScope } from '../../astUtils/reflection'; +import type { ProvideCompletionsEvent } from '../../interfaces'; +import { TokenKind } from '../../lexer/TokenKind'; +import type { XmlScope } from '../../XmlScope'; +import { util } from '../../util'; + +export class CompletionsProcessor { + constructor( + private event: ProvideCompletionsEvent + ) { + + } + + public process() { + if (isBrsFile(this.event.file) && this.event.file.isPositionNextToTokenKind(this.event.position, TokenKind.Callfunc)) { + const xmlScopes = this.event.program.getScopes().filter((s) => isXmlScope(s)) as XmlScope[]; + // is next to a @. callfunc invocation - must be an interface method. + //TODO refactor this to utilize the actual variable's component type (when available) + for (const scope of xmlScopes) { + let fileLinks = this.event.program.getStatementsForXmlFile(scope); + for (let fileLink of fileLinks) { + this.event.completions.push(scope.createCompletionFromFunctionStatement(fileLink.item)); + } + } + //no other result is possible in this case + return; + } + + //find the scopes for this file + let scopesForFile = this.event.program.getScopesForFile(this.event.file); + + //if there are no scopes, include the global scope so we at least get the built-in functions + scopesForFile = scopesForFile.length > 0 ? scopesForFile : [this.event.program.globalScope]; + + //get the completions from all scopes for this file + let allCompletions = util.flatMap( + scopesForFile.map(scope => { + return this.event.file.getCompletions(this.event.position, scope); + }), + c => c + ); + + //only keep completions common to every scope for this file + let keyCounts = new Map(); + for (let completion of allCompletions) { + let key = `${completion.label}-${completion.kind}`; + keyCounts.set(key, keyCounts.has(key) ? keyCounts.get(key) + 1 : 1); + if (keyCounts.get(key) === scopesForFile.length) { + this.event.completions.push(completion); + } + } + } +} diff --git a/src/interfaces.ts b/src/interfaces.ts index a8de701b6..8509fc2fa 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -1,4 +1,4 @@ -import type { Range, Diagnostic, CodeAction, SemanticTokenTypes, SemanticTokenModifiers, Position, Hover } from 'vscode-languageserver'; +import type { Range, Diagnostic, CodeAction, SemanticTokenTypes, SemanticTokenModifiers, Position, Hover, CompletionItem } from 'vscode-languageserver'; import type { Scope } from './Scope'; import type { BrsFile } from './files/BrsFile'; import type { XmlFile } from './files/XmlFile'; @@ -202,6 +202,19 @@ export interface CompilerPlugin { afterProgramTranspile?: (program: Program, entries: TranspileObj[], editor: AstEditor) => void; onGetCodeActions?: PluginHandler; + /** + * Emitted before the program starts collecting completions + */ + beforeProvideCompletions?: PluginHandler; + /** + * Use this event to contribute completions + */ + provideCompletions?: PluginHandler; + /** + * Emitted after the program has finished collecting completions, but before they are sent to the client + */ + afterProvideCompletions?: PluginHandler; + /** * Called before the `provideHover` hook. Use this if you need to prepare any of the in-memory objects before the `provideHover` gets called */ @@ -254,6 +267,16 @@ export interface OnGetCodeActionsEvent { codeActions: CodeAction[]; } +export interface ProvideCompletionsEvent { + program: Program; + file: TFile; + scopes: Scope[]; + position: Position; + completions: CompletionItem[]; +} +export type BeforeProvideCompletionsEvent = ProvideCompletionsEvent; +export type AfterProvideCompletionsEvent = ProvideCompletionsEvent; + export interface ProvideHoverEvent { program: Program; file: BscFile;