Skip to content
Draft
Show file tree
Hide file tree
Changes from 5 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: 4 additions & 4 deletions src/LanguageServer.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1554,7 +1554,7 @@ describe('LanguageServer', () => {
});

expect(locations.length).to.equal(1);
const location: Location = locations[0];
const location: Location = locations[0] as Location;
expect(location.uri).to.equal(functionDocument.uri);
expect(location.range.start.line).to.equal(5);
expect(location.range.start.character).to.equal(16);
Expand All @@ -1569,7 +1569,7 @@ describe('LanguageServer', () => {
});

expect(locations.length).to.equal(1);
const location: Location = locations[0];
const location: Location = locations[0] as Location;
expect(location.uri).to.equal(functionDocument.uri);
expect(location.range.start.line).to.equal(5);
expect(location.range.start.character).to.equal(16);
Expand All @@ -1594,7 +1594,7 @@ describe('LanguageServer', () => {
position: util.createPosition(3, 36)
});
expect(locations.length).to.equal(1);
const location: Location = locations[0];
const location: Location = locations[0] as Location;
expect(location.uri).to.equal(referenceDocument.uri);
expect(location.range.start.line).to.equal(2);
expect(location.range.start.character).to.equal(20);
Expand Down Expand Up @@ -1629,7 +1629,7 @@ describe('LanguageServer', () => {
position: util.createPosition(3, 30)
});
expect(locations.length).to.equal(1);
const location: Location = locations[0];
const location: Location = locations[0] as Location;
expect(location.uri).to.equal(functionDocument.uri);
expect(location.range.start.line).to.equal(2);
expect(location.range.start.character).to.equal(20);
Expand Down
2 changes: 1 addition & 1 deletion src/LanguageServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -578,7 +578,7 @@ export class LanguageServer {

const srcPath = util.uriToPath(params.textDocument.uri);

const result = this.projectManager.getDefinition({ srcPath: srcPath, position: params.position });
const result = await this.projectManager.getDefinition({ srcPath: srcPath, position: params.position });
return result;
}

Expand Down
65 changes: 61 additions & 4 deletions src/Program.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
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 } from 'vscode-languageserver';
import type { CodeAction, CompletionItem, Position, Range, SignatureInformation, Location, LocationLink, DocumentSymbol, CancellationToken, DocumentLink } from 'vscode-languageserver';
import { CancellationTokenSource, CompletionItemKind } from 'vscode-languageserver';
import type { BsConfig, FinalizedBsConfig } from './BsConfig';
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, ProvideReferencesEvent, ProvideDocumentSymbolsEvent, ProvideWorkspaceSymbolsEvent } from './interfaces';
import type { BsDiagnostic, File, FileReference, FileObj, BscFile, SemanticToken, AfterFileTranspileEvent, FileLink, ProvideHoverEvent, ProvideCompletionsEvent, Hover, ProvideDefinitionEvent, ProvideReferencesEvent, ProvideDocumentSymbolsEvent, ProvideWorkspaceSymbolsEvent, ProvideDocumentLinksEvent } from './interfaces';
import { standardizePath as s, util } from './util';
import { XmlScope } from './XmlScope';
import { DiagnosticFilterer } from './DiagnosticFilterer';
Expand Down Expand Up @@ -1001,7 +1001,7 @@ export class Program {
* Given a position in a file, if the position is sitting on some type of identifier,
* go to the definition of that identifier (where this thing was first defined)
*/
public getDefinition(srcPath: string, position: Position): Location[] {
public getDefinition(srcPath: string, position: Position): Array<Location | LocationLink> {
let file = this.getFile(srcPath);
if (!file) {
return [];
Expand All @@ -1017,12 +1017,58 @@ export class Program {
this.plugins.emit('beforeProvideDefinition', event);
this.plugins.emit('provideDefinition', event);
this.plugins.emit('afterProvideDefinition', event);

// If a document link's range contains the cursor position, wrap the definitions as
// LocationLink objects with originSelectionRange so VS Code underlines the full link range
// (e.g. the entire URI path in a script tag attribute) on Ctrl+hover rather than per-word.
if (event.definitions.length > 0) {
const documentLinks = this.getDocumentLinks(srcPath);
const matchingLink = documentLinks.find(link => util.rangeContains(link.range, position));
if (matchingLink) {
return event.definitions.map(def => {
if (isLocationLinkDef(def)) {
//already a LocationLink; overwrite originSelectionRange with the full link range
return {
originSelectionRange: matchingLink.range,
targetUri: def.targetUri,
targetRange: def.targetRange,
targetSelectionRange: def.targetSelectionRange
} as LocationLink;
}
return {
originSelectionRange: matchingLink.range,
targetUri: def.uri,
targetRange: def.range,
targetSelectionRange: def.range
} as LocationLink;
});
}
}

return event.definitions;
}

/**
* Get hover information for a file and position
* Get document links for the specified file by emitting the `provideDocumentLinks` plugin event.
* Document links are used to resolve `originSelectionRange` in go-to-definition results so that the
* full link range (e.g. an entire script tag URI path) is highlighted on Ctrl+hover.
*/
public getDocumentLinks(srcPath: string): DocumentLink[] {
const file = this.getFile(srcPath);
if (!file) {
return [];
}
const event: ProvideDocumentLinksEvent = {
program: this,
file: file,
documentLinks: []
};
this.plugins.emit('beforeProvideDocumentLinks', event);
this.plugins.emit('provideDocumentLinks', event);
this.plugins.emit('afterProvideDocumentLinks', event);
return event.documentLinks;
}

public getHover(srcPath: string, position: Position): Hover[] {
let file = this.getFile(srcPath);
let result: Hover[];
Expand Down Expand Up @@ -1546,6 +1592,17 @@ export class Program {
}
}

/**
* Type guard that returns true if the given `Location | LocationLink` is a `LocationLink`.
* A `LocationLink` has `targetUri`, `targetRange`, and `targetSelectionRange` properties,
* whereas a `Location` has `uri` and `range`.
*/
function isLocationLinkDef(def: Location | LocationLink): def is LocationLink {
return typeof (def as LocationLink).targetUri === 'string' &&
(def as LocationLink).targetRange !== undefined &&
(def as LocationLink).targetSelectionRange !== undefined;
}

export interface FileTranspileResult {
srcPath: string;
pkgPath: string;
Expand Down
4 changes: 2 additions & 2 deletions src/Scope.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { CompletionItem, Position, Range, Location } from 'vscode-languageserver';
import type { CompletionItem, Position, Range, Location, LocationLink } from 'vscode-languageserver';
import * as path from 'path';
import { CompletionItemKind } from 'vscode-languageserver';
import chalk from 'chalk';
Expand Down Expand Up @@ -1261,7 +1261,7 @@ export class Scope {
* Get the definition (where was this thing first defined) of the symbol under the position
* @deprecated use `DefinitionProvider.process()`
*/
public getDefinition(file: BscFile, position: Position): Location[] {
public getDefinition(file: BscFile, position: Position): Array<Location | LocationLink> {
// Overridden in XMLScope. Brs files use implementation in BrsFile
return [];
}
Expand Down
2 changes: 1 addition & 1 deletion src/XmlScope.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ describe('XmlScope', () => {
`);
const definition = program.getDefinition(childXmlFile.srcPath, Position.create(1, 48));
expect(definition).to.be.lengthOf(1);
expect(definition[0].uri).to.equal(util.pathToUri(parentXmlFile.srcPath));
expect((definition[0] as any).uri).to.equal(util.pathToUri(parentXmlFile.srcPath));
});
});

Expand Down
4 changes: 2 additions & 2 deletions src/XmlScope.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Location, Position } from 'vscode-languageserver';
import type { Location, LocationLink, Position } from 'vscode-languageserver';
import { Scope } from './Scope';
import { DiagnosticMessages } from './DiagnosticMessages';
import type { XmlFile } from './files/XmlFile';
Expand Down Expand Up @@ -169,7 +169,7 @@ export class XmlScope extends Scope {
* Get the definition (where was this thing first defined) of the symbol under the position
* @deprecated use `DefinitionProvider.process()`
*/
public getDefinition(file: BscFile, position: Position): Location[] {
public getDefinition(file: BscFile, position: Position): Array<Location | LocationLink> {
return new DefinitionProvider({
program: this.program,
file: file,
Expand Down
7 changes: 6 additions & 1 deletion src/bscPlugin/BscPlugin.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { isBrsFile, isXmlFile } from '../astUtils/reflection';
import type { BeforeFileTranspileEvent, Plugin, OnFileValidateEvent, OnGetCodeActionsEvent, ProvideHoverEvent, OnGetSemanticTokensEvent, OnScopeValidateEvent, ProvideCompletionsEvent, ProvideDefinitionEvent, ProvideReferencesEvent, ProvideDocumentSymbolsEvent, ProvideWorkspaceSymbolsEvent } from '../interfaces';
import type { BeforeFileTranspileEvent, Plugin, OnFileValidateEvent, OnGetCodeActionsEvent, ProvideHoverEvent, OnGetSemanticTokensEvent, OnScopeValidateEvent, ProvideCompletionsEvent, ProvideDefinitionEvent, ProvideReferencesEvent, ProvideDocumentSymbolsEvent, ProvideWorkspaceSymbolsEvent, ProvideDocumentLinksEvent } from '../interfaces';
import type { Program } from '../Program';
import { CodeActionsProcessor } from './codeActions/CodeActionsProcessor';
import { CompletionsProcessor } from './completions/CompletionsProcessor';
import { DefinitionProvider } from './definition/DefinitionProvider';
import { DocumentLinksProvider } from './documentLinks/DocumentLinksProvider';
import { DocumentSymbolProcessor } from './symbols/DocumentSymbolProcessor';
import { HoverProcessor } from './hover/HoverProcessor';
import { ReferencesProvider } from './references/ReferencesProvider';
Expand Down Expand Up @@ -42,6 +43,10 @@ export class BscPlugin implements Plugin {
new DefinitionProvider(event).process();
}

public provideDocumentLinks(event: ProvideDocumentLinksEvent) {
new DocumentLinksProvider(event).process();
}

public provideReferences(event: ProvideReferencesEvent) {
new ReferencesProvider(event).process();
}
Expand Down
53 changes: 53 additions & 0 deletions src/bscPlugin/definition/DefinitionProvider.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,4 +194,57 @@ describe('DefinitionProvider', () => {
range: util.createRange(1, 0, 1, 0)
}]);
});

it('handles script tag uri go-to-definition', () => {
const brsFile = program.setFile('components/MainScene.brs', `
sub main()
end sub
`);
const xmlFile = program.setFile('components/MainScene.xml', `
<component name="MainScene" extends="Scene">
<script type="text/brightscript" uri="pkg:/components/MainScene.brs" />
</component>
`);
// Line 2 (0-indexed): ` <script type="text/brightscript" uri="pkg:/components/MainScene.brs" />`
// The uri value range starts at the opening `"` for `pkg:/components/MainScene.brs`
const result = program.getDefinition(xmlFile.srcPath, util.createPosition(2, 60));
expect(result).to.be.lengthOf(1);
expect(result[0]).to.include({
targetUri: URI.file(brsFile.srcPath).toString()
});
// originSelectionRange should cover the full URI value (the entire filePathRange)
expect((result[0] as any).originSelectionRange).to.exist;
});

it('handles script tag uri go-to-definition with relative path', () => {
const brsFile = program.setFile('components/MainScene.brs', `
sub main()
end sub
`);
const xmlFile = program.setFile('components/MainScene.xml', `
<component name="MainScene" extends="Scene">
<script type="text/brightscript" uri="MainScene.brs" />
</component>
`);
// Line 2 (0-indexed): ` <script type="text/brightscript" uri="MainScene.brs" />`
// The uri value range starts at the opening `"` for `MainScene.brs`
const result = program.getDefinition(xmlFile.srcPath, util.createPosition(2, 54));
expect(result).to.be.lengthOf(1);
expect(result[0]).to.include({
targetUri: URI.file(brsFile.srcPath).toString()
});
expect((result[0] as any).originSelectionRange).to.exist;
});

it('returns empty array when script tag uri file is not found', () => {
const xmlFile = program.setFile('components/MainScene.xml', `
<component name="MainScene" extends="Scene">
<script type="text/brightscript" uri="pkg:/components/NotFound.brs" />
</component>
`);
// click within "pkg:/components/NotFound.brs" uri value
expect(
program.getDefinition(xmlFile.srcPath, util.createPosition(2, 60))
).to.eql([]);
});
});
18 changes: 16 additions & 2 deletions src/bscPlugin/definition/DefinitionProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { isBrsFile, isClassStatement, isDottedGetExpression, isImportStatement,
import type { BrsFile } from '../../files/BrsFile';
import type { ProvideDefinitionEvent } from '../../interfaces';
import { TokenKind } from '../../lexer/TokenKind';
import type { Location } from 'vscode-languageserver-protocol';
import type { Location, LocationLink } from 'vscode-languageserver-protocol';
import type { ClassStatement, FunctionStatement, NamespaceStatement } from '../../parser/Statement';
import { ParseMode } from '../../parser/Parser';
import util from '../../util';
Expand All @@ -16,7 +16,7 @@ export class DefinitionProvider {
private event: ProvideDefinitionEvent
) { }

public process(): Location[] {
public process(): Array<Location | LocationLink> {
if (isBrsFile(this.event.file)) {
this.brsFileGetDefinition(this.event.file);
} else if (isXmlFile(this.event.file)) {
Expand Down Expand Up @@ -258,5 +258,19 @@ export class DefinitionProvider {
uri: util.pathToUri(file.parentComponent.srcPath)
});
}

//if the position is within a script tag's uri attribute
for (const scriptImport of file.scriptTagImports) {
if (scriptImport.filePathRange && util.rangeContains(scriptImport.filePathRange, this.event.position)) {
const scriptFile = this.event.program.getFile(scriptImport.pkgPath);
if (scriptFile) {
this.event.definitions.push({
range: util.createRange(0, 0, 0, 0),
uri: util.pathToUri(scriptFile.srcPath)
});
}
break;
}
}
}
}
Loading
Loading