diff --git a/docs/plugins.md b/docs/plugins.md index c9b671a1b..a4b3ae14d 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -188,6 +188,38 @@ export interface CompilerPlugin { */ afterProvideHover?: PluginHandler; + /** + * Called before the `provideDefinition` hook + */ + beforeProvideDefinition?(event: BeforeProvideDefinitionEvent): any; + /** + * Provide one or more `Location`s where the symbol at the given position was originally defined + * @param event + */ + provideDefinition?(event: ProvideDefinitionEvent): any; + /** + * Called after `provideDefinition`. Use this if you want to intercept or sanitize the definition data provided by bsc or other plugins + * @param event + */ + afterProvideDefinition?(event: AfterProvideDefinitionEvent): any; + + + /** + * Called before the `provideReferences` hook + */ + beforeProvideReferences?(event: BeforeProvideReferencesEvent): any; + /** + * Provide all of the `Location`s where the symbol at the given position is located + * @param event + */ + provideReferences?(event: ProvideReferencesEvent): any; + /** + * Called after `provideReferences`. Use this if you want to intercept or sanitize the references data provided by bsc or other plugins + * @param event + */ + afterProvideReferences?(event: AfterProvideReferencesEvent): any; + + onGetSemanticTokens?: PluginHandler; //scope events afterScopeCreate?: (scope: Scope) => void; diff --git a/src/Program.ts b/src/Program.ts index 7af87817c..2892e3c07 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, ProvideCompletionsEvent, Hover, ProvideDefinitionEvent } from './interfaces'; +import type { BsDiagnostic, File, FileReference, FileObj, BscFile, SemanticToken, AfterFileTranspileEvent, FileLink, ProvideHoverEvent, ProvideCompletionsEvent, Hover, ProvideDefinitionEvent, ProvideReferencesEvent } from './interfaces'; import { standardizePath as s, util } from './util'; import { XmlScope } from './XmlScope'; import { DiagnosticFilterer } from './DiagnosticFilterer'; @@ -1017,14 +1017,25 @@ export class Program { return signatureHelpUtil.getSignatureHelpItems(callExpressionInfo); } - public getReferences(srcPath: string, position: Position) { + public getReferences(srcPath: string, position: Position): Location[] { //find the file let file = this.getFile(srcPath); if (!file) { return null; } - return file.getReferences(position); + const event: ProvideReferencesEvent = { + program: this, + file: file, + position: position, + references: [] + }; + + this.plugins.emit('beforeProvideReferences', event); + this.plugins.emit('provideReferences', event); + this.plugins.emit('afterProvideReferences', event); + + return event.references; } /** diff --git a/src/bscPlugin/BscPlugin.ts b/src/bscPlugin/BscPlugin.ts index f6a1a31ee..78ceea80a 100644 --- a/src/bscPlugin/BscPlugin.ts +++ b/src/bscPlugin/BscPlugin.ts @@ -1,10 +1,11 @@ import { isBrsFile, isXmlFile } from '../astUtils/reflection'; -import type { BeforeFileTranspileEvent, CompilerPlugin, OnFileValidateEvent, OnGetCodeActionsEvent, ProvideHoverEvent, OnGetSemanticTokensEvent, OnScopeValidateEvent, ProvideCompletionsEvent, ProvideDefinitionEvent } from '../interfaces'; +import type { BeforeFileTranspileEvent, CompilerPlugin, OnFileValidateEvent, OnGetCodeActionsEvent, ProvideHoverEvent, OnGetSemanticTokensEvent, OnScopeValidateEvent, ProvideCompletionsEvent, ProvideDefinitionEvent, ProvideReferencesEvent } from '../interfaces'; import type { Program } from '../Program'; import { CodeActionsProcessor } from './codeActions/CodeActionsProcessor'; import { CompletionsProcessor } from './completions/CompletionsProcessor'; import { DefinitionProvider } from './definition/DefinitionProvider'; import { HoverProcessor } from './hover/HoverProcessor'; +import { ReferencesProvider } from './references/ReferencesProvider'; import { BrsFileSemanticTokensProcessor } from './semanticTokens/BrsFileSemanticTokensProcessor'; import { BrsFilePreTranspileProcessor } from './transpile/BrsFilePreTranspileProcessor'; import { BrsFileValidator } from './validation/BrsFileValidator'; @@ -31,6 +32,10 @@ export class BscPlugin implements CompilerPlugin { new DefinitionProvider(event).process(); } + public provideReferences(event: ProvideReferencesEvent) { + new ReferencesProvider(event).process(); + } + public onGetSemanticTokens(event: OnGetSemanticTokensEvent) { if (isBrsFile(event.file)) { return new BrsFileSemanticTokensProcessor(event as any).process(); diff --git a/src/bscPlugin/references/ReferencesProvider.spec.ts b/src/bscPlugin/references/ReferencesProvider.spec.ts new file mode 100644 index 000000000..aa73bd3da --- /dev/null +++ b/src/bscPlugin/references/ReferencesProvider.spec.ts @@ -0,0 +1,58 @@ +import { expect } from '../../chai-config.spec'; +import { Program } from '../../Program'; +import { standardizePath as s, util } from '../../util'; +let rootDir = s`${process.cwd()}/rootDir`; +import { createSandbox } from 'sinon'; +import { ReferencesProvider } from './ReferencesProvider'; +import type { Location } from 'vscode-languageserver-protocol'; +import { URI } from 'vscode-uri'; +const sinon = createSandbox(); + +describe('ReferencesProvider', () => { + let program: Program; + beforeEach(() => { + program = new Program({ + rootDir: rootDir + }); + sinon.restore(); + }); + + afterEach(() => { + program.dispose(); + sinon.restore(); + }); + + it('handles unknown file type', () => { + const result = new ReferencesProvider({ + program: program, + file: undefined, + position: util.createPosition(1, 2), + references: [] + }).process(); + expect(result).to.eql([]); + }); + + it('finds references for variables in same function', () => { + const file = program.setFile('source/main.brs', ` + sub main() + name = "John" + print name + name = name + " Doe" + end sub + `); + expect( + util.sortByRange( + program.getReferences('source/main.brs', util.createPosition(3, 25)) + ).map(locationToString) + ).to.eql([ + s`${file.srcPath}:2:16-2:20`, + s`${file.srcPath}:3:22-3:26`, + s`${file.srcPath}:4:16-4:20`, + s`${file.srcPath}:4:23-4:27` + ]); + }); + + function locationToString(loc: Location) { + return `${URI.parse(loc.uri).fsPath}:${loc.range.start.line}:${loc.range.start.character}-${loc.range.end.line}:${loc.range.end.character}`; + } +}); diff --git a/src/bscPlugin/references/ReferencesProvider.ts b/src/bscPlugin/references/ReferencesProvider.ts new file mode 100644 index 000000000..27afd8480 --- /dev/null +++ b/src/bscPlugin/references/ReferencesProvider.ts @@ -0,0 +1,62 @@ +import type { BrsFile } from '../../files/BrsFile'; +import type { ProvideReferencesEvent } from '../../interfaces'; +import type { Location } from 'vscode-languageserver-protocol'; +import util from '../../util'; +import { WalkMode, createVisitor } from '../../astUtils/visitors'; +import type { XmlFile } from '../../files/XmlFile'; +import { isBrsFile, isXmlFile } from '../../astUtils/reflection'; + +export class ReferencesProvider { + constructor( + private event: ProvideReferencesEvent + ) { } + + public process(): Location[] { + if (isBrsFile(this.event.file)) { + this.brsFileGetReferences(this.event.file); + } else if (isXmlFile(this.event.file)) { + this.xmlFileGetReferences(this.event.file); + } + return this.event.references; + } + + /** + * For a position in a BrsFile, get the location where the token at that position was defined + */ + private brsFileGetReferences(file: BrsFile): void { + + const callSiteToken = file.getTokenAt(this.event.position); + + const searchFor = callSiteToken.text.toLowerCase(); + + const scopes = this.event.program.getScopesForFile(file); + + for (const scope of scopes) { + const processedFiles = new Set(); + for (const file of scope.getAllFiles()) { + if (!isBrsFile(file) || processedFiles.has(file)) { + continue; + } + processedFiles.add(file); + file.ast.walk(createVisitor({ + AssignmentStatement: (s) => { + if (s.name?.text?.toLowerCase() === searchFor) { + this.event.references.push(util.createLocation(util.pathToUri(file.srcPath), s.name.range)); + } + }, + VariableExpression: (e) => { + if (e.name.text.toLowerCase() === searchFor) { + this.event.references.push(util.createLocation(util.pathToUri(file.srcPath), e.range)); + } + } + }), { + walkMode: WalkMode.visitAllRecursive + }); + } + } + } + + private xmlFileGetReferences(file: XmlFile) { + + } +} diff --git a/src/files/BrsFile.ts b/src/files/BrsFile.ts index 1f2405b64..bacc7a952 100644 --- a/src/files/BrsFile.ts +++ b/src/files/BrsFile.ts @@ -24,13 +24,14 @@ import { BrsTranspileState } from '../parser/BrsTranspileState'; import { Preprocessor } from '../preprocessor/Preprocessor'; import { LogLevel } from '../Logger'; import { serializeError } from 'serialize-error'; -import { isCallExpression, isMethodStatement, isClassStatement, isDottedGetExpression, isFunctionExpression, isFunctionStatement, isFunctionType, isLiteralExpression, isNamespaceStatement, isStringType, isVariableExpression, isXmlFile, isImportStatement, isFieldStatement, isEnumStatement, isConstStatement } from '../astUtils/reflection'; +import { isCallExpression, isMethodStatement, isClassStatement, isDottedGetExpression, isFunctionExpression, isFunctionStatement, isFunctionType, isLiteralExpression, isNamespaceStatement, isStringType, isVariableExpression, isImportStatement, isFieldStatement, isEnumStatement, isConstStatement } from '../astUtils/reflection'; import type { BscType } from '../types/BscType'; import { createVisitor, WalkMode } from '../astUtils/visitors'; import type { DependencyGraph } from '../DependencyGraph'; import { CommentFlagProcessor } from '../CommentFlagProcessor'; import type { AstNode, Expression, Statement } from '../parser/AstNode'; import { DefinitionProvider } from '../bscPlugin/definition/DefinitionProvider'; +import { ReferencesProvider } from '../bscPlugin/references/ReferencesProvider'; /** * Holds all details about this file within the scope of the whole program @@ -1454,35 +1455,18 @@ export class BrsFile { return statement; } - public getReferences(position: Position) { - - const callSiteToken = this.getTokenAt(position); - - let locations = [] as Location[]; - - const searchFor = callSiteToken.text.toLowerCase(); - - const scopes = this.program.getScopesForFile(this); - - for (const scope of scopes) { - const processedFiles = new Set(); - for (const file of scope.getAllFiles()) { - if (isXmlFile(file) || processedFiles.has(file)) { - continue; - } - processedFiles.add(file); - file.ast.walk(createVisitor({ - VariableExpression: (e) => { - if (e.name.text.toLowerCase() === searchFor) { - locations.push(util.createLocation(util.pathToUri(file.srcPath), e.range)); - } - } - }), { - walkMode: WalkMode.visitExpressionsRecursive - }); - } - } - return locations; + /** + * Given a position in a file, if the position is sitting on some type of identifier, + * look up all references of that identifier (every place that identifier is used across the whole app) + * @deprecated use `ReferencesProvider.process()` instead + */ + public getReferences(position: Position): Location[] { + return new ReferencesProvider({ + program: this.program, + file: this, + position: position, + references: [] + }).process(); } /** diff --git a/src/interfaces.ts b/src/interfaces.ts index 6c33e84ee..05afb33e4 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -244,6 +244,23 @@ export interface CompilerPlugin { */ afterProvideDefinition?(event: AfterProvideDefinitionEvent): any; + + /** + * Called before the `provideReferences` hook + */ + beforeProvideReferences?(event: BeforeProvideReferencesEvent): any; + /** + * Provide all of the `Location`s where the symbol at the given position is located + * @param event + */ + provideReferences?(event: ProvideReferencesEvent): any; + /** + * Called after `provideReferences`. Use this if you want to intercept or sanitize the references data provided by bsc or other plugins + * @param event + */ + afterProvideReferences?(event: AfterProvideReferencesEvent): any; + + onGetSemanticTokens?: PluginHandler; //scope events afterScopeCreate?: (scope: Scope) => void; @@ -336,6 +353,24 @@ export interface ProvideDefinitionEvent { export type BeforeProvideDefinitionEvent = ProvideDefinitionEvent; export type AfterProvideDefinitionEvent = ProvideDefinitionEvent; +export interface ProvideReferencesEvent { + program: Program; + /** + * The file that the getDefinition request was invoked in + */ + file: TFile; + /** + * The position in the text document where the getDefinition request was invoked + */ + position: Position; + /** + * The list of locations for where the item at the file and position was defined + */ + references: Location[]; +} +export type BeforeProvideReferencesEvent = ProvideReferencesEvent; +export type AfterProvideReferencesEvent = ProvideReferencesEvent; + export interface OnGetSemanticTokensEvent { /**