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 support #298

Merged
merged 27 commits into from
Feb 18, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
90303ff
Addfile-based and plugin code action support
TwitchBronBron Feb 2, 2021
656e2ce
better code action message.
TwitchBronBron Feb 2, 2021
779625e
Merge branch 'master' of https://github.com/rokucommunity/brighterscr…
TwitchBronBron Feb 3, 2021
81daaf4
Add import script tag codeAction
TwitchBronBron Feb 3, 2021
84a53aa
Remove dead code
TwitchBronBron Feb 3, 2021
9d5412a
fix code actions plugin event name
TwitchBronBron Feb 3, 2021
0dfe4ae
Merge branch 'master' into getCodeActions
TwitchBronBron Feb 9, 2021
22b6eca
Remove mutating range
TwitchBronBron Feb 9, 2021
5505867
Fix broken tests.
TwitchBronBron Feb 9, 2021
55e6659
Moved codeActions into internal plugin.
TwitchBronBron Feb 10, 2021
a7a1acc
Fix CI failure issues.
TwitchBronBron Feb 10, 2021
d59f5b7
extends codeAction includes Task and ContentNode
TwitchBronBron Feb 13, 2021
5952cfe
Add extends codeAction to end of component
TwitchBronBron Feb 13, 2021
8b0a122
order codeAction events in interface
TwitchBronBron Feb 13, 2021
3841eae
Merge branch 'master' into getCodeActions
TwitchBronBron Feb 13, 2021
a88fa9d
Merge branch 'master' into getCodeActions
TwitchBronBron Feb 13, 2021
f0c6834
Fix missing logger for PluginInterface
TwitchBronBron Feb 14, 2021
0815c40
Add generic data object for diagnostics.
TwitchBronBron Feb 14, 2021
3a221ad
PluginInterface gets `addFirst` method
TwitchBronBron Feb 14, 2021
ea444c4
Created CodeActionUtil (it doesn't do much yet)
TwitchBronBron Feb 14, 2021
2816575
undo parser references change from separate PR
TwitchBronBron Feb 15, 2021
3cd55a6
Remove xml scope codeAction to simplify PR
TwitchBronBron Feb 15, 2021
2cc7122
Merge branch 'master' into getCodeActions
TwitchBronBron Feb 15, 2021
76f9b04
fix tsc error
TwitchBronBron Feb 15, 2021
f208a46
Remove unused function
TwitchBronBron Feb 15, 2021
a46c522
Merge branch 'master' into getCodeActions
TwitchBronBron Feb 17, 2021
289a445
Merge branch 'master' into getCodeActions
TwitchBronBron Feb 18, 2021
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
52 changes: 52 additions & 0 deletions src/CodeActionUtil.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import type { CodeActionKind, Diagnostic, Position, Range, WorkspaceEdit } from 'vscode-languageserver';
import { CodeAction, TextEdit } from 'vscode-languageserver';
import { URI } from 'vscode-uri';

export class CodeActionUtil {

public createCodeAction(obj: CodeActionShorthand) {
const edit = {
changes: {}
} as WorkspaceEdit;
for (const change of obj.changes) {
const uri = URI.file(change.filePath).toString();

//create the edit changes array for this uri
if (!edit.changes[uri]) {
edit.changes[uri] = [];
}
if (change.type === 'insert') {
edit.changes[uri].push(
TextEdit.insert(change.position, change.newText)
);
} else if (change.type === 'replace') {
TextEdit.replace(change.range, change.newText);
}
}
return CodeAction.create(obj.title, edit);
}
}

export interface CodeActionShorthand {
title: string;
diagnostics?: Diagnostic[];
kind?: CodeActionKind;
isPreferred?: boolean;
changes: Array<InsertChange | ReplaceChange>;
}

export interface InsertChange {
filePath: string;
newText: string;
type: 'insert';
position: Position;
}

export interface ReplaceChange {
filePath: string;
newText: string;
type: 'replace';
range: Range;
}

export const codeActionUtil = new CodeActionUtil();
18 changes: 14 additions & 4 deletions src/DiagnosticMessages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ export let DiagnosticMessages = {
callToUnknownFunction: (name: string, scopeName: string) => ({
message: `Cannot find function with name '${name}' when this file is included in scope '${scopeName}'`,
code: 1001,
data: {
functionName: name
},
severity: DiagnosticSeverity.Error
}),
mismatchArgumentCount: (expectedCount: number | string, actualCount: number) => ({
Expand Down Expand Up @@ -623,15 +626,22 @@ export let DiagnosticMessages = {
})
};

let allCodes = [] as number[];
export const DiagnosticCodeMap = {} as Record<keyof (typeof DiagnosticMessages), number>;
export let diagnosticCodes = [] as number[];
for (let key in DiagnosticMessages) {
allCodes.push(DiagnosticMessages[key]().code);
diagnosticCodes.push(DiagnosticMessages[key]().code);
DiagnosticCodeMap[key] = DiagnosticMessages[key]().code;
}

export let diagnosticCodes = allCodes;

export interface DiagnosticInfo {
message: string;
code: number;
severity: DiagnosticSeverity;
}

/**
* Provides easy type support for the return value of any DiagnosticMessage function.
* 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]>;
1 change: 1 addition & 0 deletions src/LanguageServer.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ describe('LanguageServer', () => {
sendNotification: () => null,
sendDiagnostics: () => null,
onExecuteCommand: () => null,
onCodeAction: () => null,
onDidOpenTextDocument: () => null,
onDidChangeTextDocument: () => null,
onDidCloseTextDocument: () => null,
Expand Down
27 changes: 25 additions & 2 deletions src/LanguageServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,17 @@ import type {
DocumentSymbolParams,
ReferenceParams,
SignatureHelp,
SignatureHelpParams
SignatureHelpParams,
CodeActionParams
} from 'vscode-languageserver';
import {
createConnection,
DidChangeConfigurationNotification,
FileChangeType,
ProposedFeatures,
TextDocuments,
TextDocumentSyncKind
TextDocumentSyncKind,
CodeActionKind
} from 'vscode-languageserver';
import { URI } from 'vscode-uri';
import { TextDocument } from 'vscode-languageserver-textdocument';
Expand Down Expand Up @@ -143,6 +145,8 @@ export class LanguageServer {

this.connection.onReferences(this.onReferences.bind(this));

this.connection.onCodeAction(this.onCodeAction.bind(this));

/*
this.connection.onDidOpenTextDocument((params) => {
// A text document got opened in VSCode.
Expand Down Expand Up @@ -196,6 +200,9 @@ export class LanguageServer {
documentSymbolProvider: true,
workspaceSymbolProvider: true,
referencesProvider: true,
codeActionProvider: {
codeActionKinds: [CodeActionKind.Refactor]
},
signatureHelpProvider: {
triggerCharacters: ['(', ',']
},
Expand Down Expand Up @@ -543,6 +550,22 @@ export class LanguageServer {
return item;
}

private async onCodeAction(params: CodeActionParams) {
//ensure programs are initialized
await this.waitAllProgramFirstRuns();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These awaits will potentially accumulate code action requests - should there instead be a mechanism to ignore codeactions when the file isn't ready? We may "miss" some actions but just moving the cursor around will produce then again.

Copy link
Member Author

@TwitchBronBron TwitchBronBron Feb 18, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The language server protocol might already account for this and prevent additional requests if the first one hasn't resolved yet. So I'll test that first, and if we DO get multiple requests at the same time, then I'll add some code to ignore code actions that arrive before the programs are finished with their first runs.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just tested, and I can't get these to queue up. There's a waitAllProgramFirstRuns() in the onInitialized, so the vscode doesn't even send codeAction events until that's resolved, so this line is really a "just in case" measure.


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

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

let codeActions = this
.getWorkspaces()
.flatMap(workspace => workspace.builder.program.getCodeActions(filePath, params.range));

return codeActions;
}

/**
* Reload all specified workspaces, or all workspaces if no workspaces are specified
*/
Expand Down
12 changes: 12 additions & 0 deletions src/PluginInterface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,18 @@ export default class PluginInterface<T extends CompilerPlugin = CompilerPlugin>
}
}

/**
* Add a plugin to the beginning of the list of plugins
*/
public addFirst(plugin: CompilerPlugin) {
if (!this.has(plugin)) {
this.plugins.unshift(plugin);
}
}

/**
* Add a plugin to the end of the list of plugins
*/
public add(plugin: CompilerPlugin) {
if (!this.has(plugin)) {
this.plugins.push(plugin);
Expand Down
34 changes: 32 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 { CompletionItem, Position, SignatureInformation } from 'vscode-languageserver';
import type { CodeAction, CompletionItem, Position, Range, SignatureInformation } from 'vscode-languageserver';
import { Location, CompletionItemKind } from 'vscode-languageserver';
import type { BsConfig } from './BsConfig';
import { Scope } from './Scope';
Expand All @@ -24,6 +24,7 @@ import { isBrsFile, isXmlFile, isClassMethodStatement, isXmlScope } from './astU
import type { FunctionStatement, Statement } from './parser/Statement';
import { ParseMode } from './parser';
import { TokenKind } from './lexer';
import { BscPlugin } from './bscPlugin/BscPlugin';
const startOfSourcePkgPath = `source${path.sep}`;

export interface SourceObj {
Expand Down Expand Up @@ -67,7 +68,10 @@ export class Program {
) {
this.options = util.normalizeConfig(options);
this.logger = logger || new Logger(options.logLevel as LogLevel);
this.plugins = plugins || new PluginInterface([], undefined);
this.plugins = plugins || new PluginInterface([], this.logger);

//inject the bsc plugin as the first plugin in the stack.
this.plugins.addFirst(new BscPlugin());

//normalize the root dir path
this.options.rootDir = util.getRootDir(this.options);
Expand Down Expand Up @@ -734,6 +738,32 @@ export class Program {
return Promise.resolve(file.getHover(position));
}

/**
* Compute code actions for the given file and range
*/
public getCodeActions(pathAbsolute: string, range: Range) {
const codeActions = [] as CodeAction[];
const file = this.getFile(pathAbsolute);

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);
}
}

this.plugins.emit('afterProgramGetCodeActions', this, file, range, codeActions);
return codeActions;
}

public getSignatureHelp(filepath: string, position: Position): SignatureInfoObj[] {
let file: BrsFile = this.getFile(filepath);
if (!file || !isBrsFile(file)) {
Expand Down
7 changes: 6 additions & 1 deletion src/Scope.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { CompletionItem, Position, Range } from 'vscode-languageserver';
import type { CodeAction, CompletionItem, Position, Range } from 'vscode-languageserver';
import { CompletionItemKind, Location } from 'vscode-languageserver';
import chalk from 'chalk';
import type { DiagnosticInfo } from './DiagnosticMessages';
Expand Down Expand Up @@ -243,6 +243,11 @@ 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: 6 additions & 1 deletion 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 { CodeAction, Location, Position, Range } from 'vscode-languageserver';
import { Scope } from './Scope';
import { DiagnosticMessages } from './DiagnosticMessages';
import type { XmlFile } from './files/XmlFile';
Expand Down Expand Up @@ -153,6 +153,11 @@ 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
14 changes: 14 additions & 0 deletions src/bscPlugin/BscPlugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import type { CodeAction, Range } from 'vscode-languageserver';
import { isXmlFile } from '../astUtils/reflection';
import type { BscFile, BsDiagnostic, CompilerPlugin } from '../interfaces';
import { XmlFileCodeActionsProcessor } from './codeActions/XmlFileCodeActionsProcessor';

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();
}
}
}
71 changes: 71 additions & 0 deletions src/bscPlugin/codeActions/XmlFileCodeActionsProcessor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import type { CodeAction, Range } from 'vscode-languageserver';
import { CodeActionKind } from 'vscode-languageserver';
import { codeActionUtil } from '../../CodeActionUtil';
import { DiagnosticCodeMap } from '../../DiagnosticMessages';
import type { XmlFile } from '../../files/XmlFile';
import type { BsDiagnostic } from '../../interfaces';

export class XmlFileCodeActionsProcessor {
public constructor(
public file: XmlFile,
public range: Range,
public diagnostics: BsDiagnostic[],
public codeActions: CodeAction[]
) {

}

public process() {
for (const diagnostic of this.diagnostics) {
if (diagnostic.code === DiagnosticCodeMap.xmlComponentMissingExtendsAttribute) {
this.addMissingExtends();
}
}
}

private addMissingExtends() {
const { component } = this.file.parser.ast;
//inject new attribute after the final attribute, or after the `<component` if there are no attributes
const pos = (component.attributes[component.attributes.length - 1] ?? component.tag).range.end;
this.codeActions.push(
codeActionUtil.createCodeAction({
title: `Extend "Group"`,
// diagnostics: [diagnostic],
isPreferred: true,
kind: CodeActionKind.QuickFix,
changes: [{
type: 'insert',
filePath: this.file.pathAbsolute,
position: pos,
newText: ' extends="Group"'
}]
})
);
this.codeActions.push(
codeActionUtil.createCodeAction({
title: `Extend "Task"`,
// diagnostics: [diagnostic],
kind: CodeActionKind.QuickFix,
changes: [{
type: 'insert',
filePath: this.file.pathAbsolute,
position: pos,
newText: ' extends="Task"'
}]
})
);
this.codeActions.push(
codeActionUtil.createCodeAction({
title: `Extend "ContentNode"`,
// diagnostics: [diagnostic],
kind: CodeActionKind.QuickFix,
changes: [{
type: 'insert',
filePath: this.file.pathAbsolute,
position: pos,
newText: ' extends="ContentNode"'
}]
})
);
}
}
7 changes: 6 additions & 1 deletion src/files/BrsFile.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { CodeWithSourceMap } from 'source-map';
import { SourceNode } from 'source-map';
import type { CompletionItem, Hover, Range, Position } from 'vscode-languageserver';
import type { CompletionItem, Hover, Range, Position, CodeAction } from 'vscode-languageserver';
import { CompletionItemKind, SymbolKind, Location, SignatureInformation, ParameterInformation, DocumentSymbol, SymbolInformation } from 'vscode-languageserver';
import chalk from 'chalk';
import * as path from 'path';
Expand Down Expand Up @@ -1509,6 +1509,11 @@ export class BrsFile {
}
}

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

public getSignatureHelpForNamespaceMethods(callableName: string, dottedGetText: string, scope: Scope): { key: string; signature: SignatureInformation }[] {
if (!dottedGetText) {
return [];
Expand Down
Loading