Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add codeAction for bs imports #347

Merged
merged 10 commits into from
Mar 10, 2021
6 changes: 1 addition & 5 deletions docs/plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,11 +93,7 @@ After file addition/removal (note: throttled/debounced):
- `afterProgramValidate`

Code Actions
- `beforeProgramGetCodeActions`
- `onFileGetCodeActions`
- for each scope that includes the file
- `onScopeGetCodeActions`
- `afterProgramGetCodeActions`
- `onProgramGetCodeActions`

## Compiler API

Expand Down
8 changes: 5 additions & 3 deletions src/DiagnosticMessages.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
/* eslint-disable camelcase */

import type { Position } from 'vscode-languageserver';
import { DiagnosticSeverity } from 'vscode-languageserver';
import type { BsDiagnostic } from './interfaces';
import type { TokenKind } from './lexer/TokenKind';

/**
Expand Down Expand Up @@ -644,4 +643,7 @@ export interface DiagnosticInfo {
* The second type parameter is optional, but allows plugins to pass in their own
* DiagnosticMessages-like object in order to get the same type support
*/
export type DiagnosticMessageType<K extends keyof D, D extends Record<string, (...args: any) => any> = typeof DiagnosticMessages> = ReturnType<D[K]>;
export type DiagnosticMessageType<K extends keyof D, D extends Record<string, (...args: any) => any> = typeof DiagnosticMessages> =
ReturnType<D[K]> &
//include the missing properties from BsDiagnostic
Pick<BsDiagnostic, 'range' | 'file' | 'relatedInformation' | 'tags'>;
28 changes: 12 additions & 16 deletions src/LanguageServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ import {
} from 'vscode-languageserver';
import { URI } from 'vscode-uri';
import { TextDocument } from 'vscode-languageserver-textdocument';

import type { BsConfig } from './BsConfig';
import { Deferred } from './deferred';
import { DiagnosticMessages } from './DiagnosticMessages';
Expand Down Expand Up @@ -554,17 +553,23 @@ export class LanguageServer {
//ensure programs are initialized
await this.waitAllProgramFirstRuns();

let filePath = util.uriToPath(params.textDocument.uri);
let srcPath = util.uriToPath(params.textDocument.uri);

//wait until the file has settled
await this.keyedThrottler.onIdleOnce(filePath, true);
await this.keyedThrottler.onIdleOnce(srcPath, true);

let codeActions = this
const codeActions = this
.getWorkspaces()
//skip programs that don't have this file
.filter(x => x.builder.program.hasFile(filePath))
.flatMap(workspace => workspace.builder.program.getCodeActions(filePath, params.range));
.filter(x => x.builder?.program?.hasFile(srcPath))
.flatMap(workspace => workspace.builder.program.getCodeActions(srcPath, params.range));

//clone the diagnostics for each code action, since certain diagnostics can have circular reference properties that kill the language server if serialized
for (const codeAction of codeActions) {
if (codeAction.diagnostics) {
codeAction.diagnostics = codeAction.diagnostics.map(x => util.toDiagnostic(x));
}
}
return codeActions;
}

Expand Down Expand Up @@ -1051,16 +1056,7 @@ export class LanguageServer {
const patch = await this.diagnosticCollection.getPatch(this.workspaces);

for (let filePath in patch) {
const diagnostics = patch[filePath].map(d => {
return {
severity: d.severity,
range: d.range,
message: d.message,
relatedInformation: d.relatedInformation,
code: d.code,
source: 'brs'
};
});
const diagnostics = patch[filePath].map(d => util.toDiagnostic(d));

this.connection.sendDiagnostics({
uri: URI.file(filePath).toString(),
Expand Down
50 changes: 35 additions & 15 deletions src/Program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -783,23 +783,24 @@ export class Program {
const codeActions = [] as CodeAction[];
const file = this.getFile(pathAbsolute);
if (file) {
const diagnostics = this
//get all current diagnostics (filtered by diagnostic filters)
.getDiagnostics()
//only keep diagnostics related to this file
.filter(x => x.file === file)
//only keep diagnostics that touch this range
.filter(x => util.rangesIntersect(x.range, range));

this.plugins.emit('beforeProgramGetCodeActions', this, file, range, codeActions);

//get code actions from the file
file.getCodeActions(range, codeActions);

//get code actions from every scope this file is a member of
for (let key in this.scopes) {
let scope = this.scopes[key];

if (scope.hasFile(file)) {
//get code actions from each scope this file is a member of
scope.getCodeActions(file, range, codeActions);
}
}
const scopes = this.getScopesForFile(file);

this.plugins.emit('afterProgramGetCodeActions', this, file, range, codeActions);
this.plugins.emit('onGetCodeActions', {
program: this,
file: file,
range: range,
diagnostics: diagnostics,
scopes: scopes,
codeActions: codeActions
});
}
return codeActions;
}
Expand Down Expand Up @@ -1179,6 +1180,25 @@ export class Program {
this.plugins.emit('afterProgramTranspile', this, entries);
}

/**
* Find a list of files in the program that have a function with the given name (case INsensitive)
*/
public findFilesForFunction(functionName: string) {
const files = [] as BscFile[];
const lowerFunctionName = functionName.toLowerCase();
//find every file with this function defined
for (const file of Object.values(this.files)) {
if (isBrsFile(file)) {
//TODO handle namespace-relative function calls
//if the file has a function with this name
if (file.parser.references.functionStatementLookup.get(lowerFunctionName) !== undefined) {
files.push(file);
}
}
}
return files;
}

/**
* Get a map of the manifest information
*/
Expand Down
7 changes: 1 addition & 6 deletions src/Scope.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { CodeAction, CompletionItem, Position, Range } from 'vscode-languageserver';
import type { CompletionItem, Position, Range } from 'vscode-languageserver';
import * as path from 'path';
import { CompletionItemKind, Location } from 'vscode-languageserver';
import chalk from 'chalk';
Expand Down Expand Up @@ -244,11 +244,6 @@ export class Scope {
this.diagnostics.push(...diagnostics);
}

public getCodeActions(file: BscFile, range: Range, codeActions: CodeAction[]) {
const diagnostics = this.diagnostics.filter(x => x.range?.start.line === range.start.line);
this.program.plugins.emit('onScopeGetCodeActions', this, file, range, diagnostics, codeActions);
}

/**
* Get the list of callables available in this scope (either declared in this scope or in a parent scope)
*/
Expand Down
7 changes: 1 addition & 6 deletions src/XmlScope.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { CodeAction, Location, Position, Range } from 'vscode-languageserver';
import type { Location, Position } from 'vscode-languageserver';
import { Scope } from './Scope';
import { DiagnosticMessages } from './DiagnosticMessages';
import type { XmlFile } from './files/XmlFile';
Expand Down Expand Up @@ -153,11 +153,6 @@ export class XmlScope extends Scope {
});
}

public getCodeActions(file: BscFile, range: Range, codeActions: CodeAction[]) {
const relevantDiagnostics = this.diagnostics.filter(x => x.range?.start.line === range.start.line);
this.program.plugins.emit('onScopeGetCodeActions', this, file, range, relevantDiagnostics, codeActions);
}

/**
* Get the definition (where was this thing first defined) of the symbol under the position
*/
Expand Down
12 changes: 4 additions & 8 deletions src/bscPlugin/BscPlugin.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,10 @@
import type { CodeAction, Range } from 'vscode-languageserver';
import { isXmlFile } from '../astUtils/reflection';
import type { BscFile, BsDiagnostic, CompilerPlugin } from '../interfaces';
import { XmlFileCodeActionsProcessor } from './codeActions/XmlFileCodeActionsProcessor';
import type { OnGetCodeActionsEvent, CompilerPlugin } from '../interfaces';
import { CodeActionsProcessor } from './codeActions/CodeActionsProcessor';

export class BscPlugin implements CompilerPlugin {
public name = 'BscPlugin';

public onFileGetCodeActions(file: BscFile, range: Range, diagnostics: BsDiagnostic[], codeActions: CodeAction[]) {
if (isXmlFile(file)) {
new XmlFileCodeActionsProcessor(file, range, diagnostics, codeActions).process();
}
public onGetCodeActions(event: OnGetCodeActionsEvent) {
new CodeActionsProcessor(event).process();
}
}
131 changes: 131 additions & 0 deletions src/bscPlugin/codeActions/CodeActionsProcessor.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import { expect } from 'chai';
import { URI } from 'vscode-uri';
import { Program } from '../../Program';
import { expectCodeActions, trim } from '../../testHelpers.spec';
import { standardizePath as s, util } from '../../util';

const rootDir = s`${process.cwd()}/.tmp/rootDir`;
describe('CodeActionsProcessor', () => {
let program: Program;
beforeEach(() => {
program = new Program({
rootDir: rootDir
});
});
afterEach(() => {
program.dispose();
});

describe('getCodeActions', () => {
it('suggests `extends=Group`', () => {
const file = program.addOrReplaceFile('components/comp1.xml', trim`
<?xml version="1.0" encoding="utf-8" ?>
<component name="comp1">
</component>
`);
expectCodeActions(() => {
program.getCodeActions(
file.pathAbsolute,
//<comp|onent name="comp1">
util.createRange(1, 5, 1, 5)
);
}, [{
title: `Extend "Group"`,
isPreferred: true,
kind: 'quickfix',
changes: [{
filePath: s`${rootDir}/components/comp1.xml`,
newText: ' extends="Group"',
type: 'insert',
//<component name="comp1"|>
position: util.createPosition(1, 23)
}]
}, {
title: `Extend "Task"`,
kind: 'quickfix',
changes: [{
filePath: s`${rootDir}/components/comp1.xml`,
newText: ' extends="Task"',
type: 'insert',
//<component name="comp1"|>
position: util.createPosition(1, 23)
}]
}, {
title: `Extend "ContentNode"`,
kind: 'quickfix',
changes: [{
filePath: s`${rootDir}/components/comp1.xml`,
newText: ' extends="ContentNode"',
type: 'insert',
//<component name="comp1"|>
position: util.createPosition(1, 23)
}]
}]);
});

it('adds attribute at end of component with multiple attributes`', () => {
const file = program.addOrReplaceFile('components/comp1.xml', trim`
<?xml version="1.0" encoding="utf-8" ?>
<component name="comp1" attr2="attr3" attr3="attr3">
</component>
`);
const codeActions = program.getCodeActions(
file.pathAbsolute,
//<comp|onent name="comp1">
util.createRange(1, 5, 1, 5)
);

expect(
codeActions[0].edit.changes[URI.file(s`${rootDir}/components/comp1.xml`).toString()][0].range
).to.eql(
util.createRange(1, 51, 1, 51)
);
});
});

it('does not produce duplicate code actions for bs imports', () => {
//define the function in two files
program.addOrReplaceFile('components/lib1.brs', `
sub doSomething()
end sub
`);
program.addOrReplaceFile('components/lib2.brs', `
sub doSomething()
end sub
`);

//use the function in this file
const componentCommonFile = program.addOrReplaceFile('components/ComponentCommon.bs', `
sub init()
doSomething()
end sub
`);

//import the file in two scopes
program.addOrReplaceFile('components/comp1.xml', trim`
<?xml version="1.0" encoding="utf-8" ?>
<component name="ChildScene">
<script uri="ComponentCommon.bs" />
</component>
`);
program.addOrReplaceFile('components/comp2.xml', trim`
<?xml version="1.0" encoding="utf-8" ?>
<component name="ChildScene">
<script uri="ComponentCommon.bs" />
</component>
`);

program.validate();

//we should only get each file import suggestion exactly once
const codeActions = program.getCodeActions(
componentCommonFile.pathAbsolute,
// doSome|thing()
util.createRange(2, 22, 2, 22)
);
expect(codeActions.map(x => x.title).sort()).to.eql([
`import "pkg:/components/lib1.brs"`,
`import "pkg:/components/lib2.brs"`
]);
});
});
Loading