Skip to content
Draft
Show file tree
Hide file tree
Changes from 4 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
15 changes: 14 additions & 1 deletion src/LanguageServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ import type {
CompletionList,
CancellationToken,
DidChangeConfigurationParams,
DidChangeConfigurationRegistrationOptions
DidChangeConfigurationRegistrationOptions,
DocumentLinkParams,
DocumentLink
} from 'vscode-languageserver/node';
import {
SemanticTokensRequest,
Expand Down Expand Up @@ -221,6 +223,9 @@ export class LanguageServer {
},
definitionProvider: true,
hoverProvider: true,
documentLinkProvider: {
resolveProvider: false
},
executeCommandProvider: {
commands: [
CustomCommands.TranspileFile
Expand Down Expand Up @@ -582,6 +587,14 @@ export class LanguageServer {
return result;
}

@AddStackToErrorMessage
public async onDocumentLinks(params: DocumentLinkParams): Promise<DocumentLink[]> {
this.logger.debug('onDocumentLinks', params);

const srcPath = util.uriToPath(params.textDocument.uri);
return this.projectManager.getDocumentLinks({ srcPath: srcPath });
}

@AddStackToErrorMessage
public async onSignatureHelp(params: SignatureHelpParams) {
this.logger.debug('onSignatureHelp', params);
Expand Down
23 changes: 21 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 } from 'vscode-languageserver';
import type { CodeAction, CompletionItem, Position, Range, SignatureInformation, Location, DocumentSymbol, CancellationToken, DocumentLink } from 'vscode-languageserver';
import { CancellationTokenSource, CompletionItemKind } from 'vscode-languageserver';
import type { BsConfig, FinalizedBsConfig } from './BsConfig';
import { Scope } from './Scope';
Expand Down Expand Up @@ -1021,8 +1021,27 @@ export class Program {
}

/**
* Get hover information for a file and position
* Get document links (clickable URI ranges) for the specified file.
* This is used to make script tag URI attributes in XML files single-clickable links.
*/
public getDocumentLinks(srcPath: string): DocumentLink[] {
const file = this.getFile(srcPath);
if (!isXmlFile(file)) {
return [];
}
const links: DocumentLink[] = [];
for (const scriptImport of file.scriptTagImports) {
if (scriptImport.filePathRange) {
const scriptFile = this.getFile(scriptImport.pkgPath);
links.push({
range: scriptImport.filePathRange,
target: scriptFile ? util.pathToUri(scriptFile.srcPath) : undefined
});
}
}
return links;
}

public getHover(srcPath: string, position: Position): Hover[] {
let file = this.getFile(srcPath);
let result: Hover[];
Expand Down
104 changes: 104 additions & 0 deletions src/bscPlugin/definition/DefinitionProvider.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,4 +194,108 @@ 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`
expect(
program.getDefinition(xmlFile.srcPath, util.createPosition(2, 60))
).to.eql([{
uri: URI.file(brsFile.srcPath).toString(),
range: util.createRange(0, 0, 0, 0)
}]);
});

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`
expect(
program.getDefinition(xmlFile.srcPath, util.createPosition(2, 54))
).to.eql([{
uri: URI.file(brsFile.srcPath).toString(),
range: util.createRange(0, 0, 0, 0)
}]);
});

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([]);
});

describe('getDocumentLinks', () => {
it('returns document links for script tag uris', () => {
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>
`);
const links = program.getDocumentLinks(xmlFile.srcPath);
expect(links).to.be.lengthOf(1);
expect(links[0].target).to.equal(URI.file(brsFile.srcPath).toString());
});

it('returns document link with undefined target when script 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>
`);
const links = program.getDocumentLinks(xmlFile.srcPath);
expect(links).to.be.lengthOf(1);
expect(links[0].target).to.be.undefined;
});

it('returns empty array for non-xml files', () => {
const brsFile = program.setFile('source/main.brs', `
sub main()
end sub
`);
const links = program.getDocumentLinks(brsFile.srcPath);
expect(links).to.eql([]);
});

it('returns multiple links for multiple script tags', () => {
const brsFile1 = program.setFile('components/MainScene.brs', ``);
const brsFile2 = program.setFile('components/Helpers.brs', ``);
const xmlFile = program.setFile('components/MainScene.xml', `
<component name="MainScene" extends="Scene">
<script type="text/brightscript" uri="MainScene.brs" />
<script type="text/brightscript" uri="Helpers.brs" />
</component>
`);
const links = program.getDocumentLinks(xmlFile.srcPath);
expect(links).to.be.lengthOf(2);
expect(links[0].target).to.equal(URI.file(brsFile1.srcPath).toString());
expect(links[1].target).to.equal(URI.file(brsFile2.srcPath).toString());
});
});
});
14 changes: 14 additions & 0 deletions src/bscPlugin/definition/DefinitionProvider.ts
Original file line number Diff line number Diff line change
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;
}
}
}
}
7 changes: 6 additions & 1 deletion src/lsp/LspProject.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Diagnostic, Position, Range, Location, DocumentSymbol, WorkspaceSymbol, CodeAction, CompletionList } from 'vscode-languageserver-protocol';
import type { Diagnostic, Position, Range, Location, DocumentSymbol, WorkspaceSymbol, CodeAction, CompletionList, DocumentLink } from 'vscode-languageserver-protocol';
import type { Hover, MaybePromise, SemanticToken } from '../interfaces';
import type { DocumentAction, DocumentActionWithStatus } from './DocumentManager';
import type { FileTranspileResult, SignatureInfoObj } from '../Program';
Expand Down Expand Up @@ -142,6 +142,11 @@ export interface LspProject {
*/
getCodeActions(options: { srcPath: string; range: Range }): Promise<CodeAction[]>;

/**
* Get document links (clickable URI ranges) for the specified file
*/
getDocumentLinks(options: { srcPath: string }): MaybePromise<DocumentLink[]>;

/**
* Get the completions for the specified file and position
*/
Expand Down
7 changes: 7 additions & 0 deletions src/lsp/Project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -432,6 +432,13 @@ export class Project implements LspProject {
}
}

public async getDocumentLinks(options: { srcPath: string }) {
await this.onIdle();
if (this.builder.program.hasFile(options.srcPath)) {
return this.builder.program.getDocumentLinks(options.srcPath);
}
}

public async getCompletions(options: { srcPath: string; position: Position }): Promise<CompletionList> {
await this.onIdle();

Expand Down
16 changes: 15 additions & 1 deletion src/lsp/ProjectManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import type { LspDiagnostic, LspProject, ProjectConfig } from './LspProject';
import { Project } from './Project';
import { WorkerThreadProject } from './worker/WorkerThreadProject';
import { FileChangeType } from 'vscode-languageserver-protocol';
import type { Hover, Position, Range, Location, SignatureHelp, DocumentSymbol, SymbolInformation, WorkspaceSymbol, CompletionList, CancellationToken } from 'vscode-languageserver-protocol';
import type { Hover, Position, Range, Location, SignatureHelp, DocumentSymbol, SymbolInformation, WorkspaceSymbol, CompletionList, CancellationToken, DocumentLink } from 'vscode-languageserver-protocol';
import { Deferred } from '../deferred';
import type { DocumentActionWithStatus, FlushEvent } from './DocumentManager';
import { DocumentManager } from './DocumentManager';
Expand Down Expand Up @@ -668,6 +668,20 @@ export class ProjectManager {
return result;
}

@TrackBusyStatus
public async getDocumentLinks(options: { srcPath: string }): Promise<DocumentLink[]> {
//wait for all pending syncs to finish
await this.onIdle();

//Ask every project for document links, keep whichever one responds first that has a valid response
let result = await util.promiseRaceMatch(
this.projects.map(x => x.getDocumentLinks(options)),
//keep the first non-falsey result
(result) => !!result
);
return result ?? [];
}

/**
* Scan a given workspace for all `bsconfig.json` files. If at least one is found, then only folders who have bsconfig.json are returned.
* If none are found, then the workspaceFolder itself is treated as a project
Expand Down
6 changes: 5 additions & 1 deletion src/lsp/worker/WorkerThreadProject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import type { Hover, MaybePromise, SemanticToken } from '../../interfaces';
import type { DocumentAction, DocumentActionWithStatus } from '../DocumentManager';
import { Deferred } from '../../deferred';
import type { FileTranspileResult, SignatureInfoObj } from '../../Program';
import type { Position, Range, Location, DocumentSymbol, WorkspaceSymbol, CodeAction, CompletionList } from 'vscode-languageserver-protocol';
import type { Position, Range, Location, DocumentSymbol, WorkspaceSymbol, CodeAction, CompletionList, DocumentLink } from 'vscode-languageserver-protocol';
import type { Logger } from '../../logging';
import { createLogger } from '../../logging';
import * as fsExtra from 'fs-extra';
Expand Down Expand Up @@ -244,6 +244,10 @@ export class WorkerThreadProject implements LspProject {
return this.sendStandardRequest<CodeAction[]>('getCodeActions', options);
}

public async getDocumentLinks(options: { srcPath: string }): Promise<DocumentLink[]> {
return this.sendStandardRequest<DocumentLink[]>('getDocumentLinks', options);
}

public async getCompletions(options: { srcPath: string; position: Position }): Promise<CompletionList> {
return this.sendStandardRequest<CompletionList>('getCompletions', options);
}
Expand Down
Loading