From 90303ff1cac6fcd9b0724756b3c6e5e4f19922df Mon Sep 17 00:00:00 2001 From: Bronley Date: Tue, 2 Feb 2021 17:02:44 -0500 Subject: [PATCH 01/20] Addfile-based and plugin code action support --- src/LanguageServer.ts | 46 +++++++++++++++++++++++++++++++++++++++++-- src/Program.ts | 22 ++++++++++++++++++++- src/files/BrsFile.ts | 6 +++++- src/files/XmlFile.ts | 35 +++++++++++++++++++++++++++++++- src/interfaces.ts | 26 +++++++++++++++++++++++- src/util.ts | 26 ++++++++++++++++++++++-- 6 files changed, 153 insertions(+), 8 deletions(-) diff --git a/src/LanguageServer.ts b/src/LanguageServer.ts index 3d2859093..1138a7cbb 100644 --- a/src/LanguageServer.ts +++ b/src/LanguageServer.ts @@ -17,7 +17,8 @@ import type { DocumentSymbolParams, ReferenceParams, SignatureHelp, - SignatureHelpParams + SignatureHelpParams, + CodeActionParams } from 'vscode-languageserver'; import { createConnection, @@ -25,7 +26,8 @@ import { FileChangeType, ProposedFeatures, TextDocuments, - TextDocumentSyncKind + TextDocumentSyncKind, + CodeActionKind } from 'vscode-languageserver'; import { URI } from 'vscode-uri'; import { TextDocument } from 'vscode-languageserver-textdocument'; @@ -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. @@ -196,6 +200,9 @@ export class LanguageServer { documentSymbolProvider: true, workspaceSymbolProvider: true, referencesProvider: true, + codeActionProvider: { + codeActionKinds: [CodeActionKind.Refactor] + }, signatureHelpProvider: { triggerCharacters: ['(', ','] }, @@ -543,6 +550,41 @@ export class LanguageServer { return item; } + private async onCodeAction(params: CodeActionParams) { + //ensure programs are initialized + await this.waitAllProgramFirstRuns(); + + 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 [ + // util.createCodeAction({ + // title: 'Add extends attribute', + // changes: [{ + // type: 'insert', + // filePath: URI.parse(params.textDocument.uri).fsPath, + // newText: ' extends="Group"', + // position: util.createPosition(1, 32) + // }] + // }) + // ]; + // return [CodeAction.create('Add extends attribute', { + // changes: { + // [params.textDocument.uri]: [ + // TextEdit.insert(util.createPosition(1, 32), ' extends="Group"') + // ] + // } + // }) + // ]; + return codeActions; + } + /** * Reload all specified workspaces, or all workspaces if no workspaces are specified */ diff --git a/src/Program.ts b/src/Program.ts index e3570aaed..35574b545 100644 --- a/src/Program.ts +++ b/src/Program.ts @@ -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'; @@ -733,6 +733,26 @@ 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('beforeGetCodeActions', file, range, codeActions); + + //get code actions from the file + codeActions.push( + ...file.getCodeActions(range) + ); + + //TODO get code actions from each scope + + this.plugins.emit('beforeGetCodeActions', file, range, codeActions); + return codeActions; + } + public getSignatureHelp(filepath: string, position: Position): SignatureInfoObj[] { let file: BrsFile = this.getFile(filepath); if (!file || !isBrsFile(file)) { diff --git a/src/files/BrsFile.ts b/src/files/BrsFile.ts index 18e5058bf..12eebae7a 100644 --- a/src/files/BrsFile.ts +++ b/src/files/BrsFile.ts @@ -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'; @@ -1466,6 +1466,10 @@ export class BrsFile { } } + public getCodeActions(range: Range): CodeAction[] { + return []; + } + public getSignatureHelpForNamespaceMethods(callableName: string, dottedGetText: string, scope: Scope): { key: string; signature: SignatureInformation }[] { if (!dottedGetText) { return []; diff --git a/src/files/XmlFile.ts b/src/files/XmlFile.ts index 519a24400..acf1f2a5e 100644 --- a/src/files/XmlFile.ts +++ b/src/files/XmlFile.ts @@ -1,7 +1,8 @@ import * as path from 'path'; import type { CodeWithSourceMap } from 'source-map'; import { SourceNode } from 'source-map'; -import type { CompletionItem, Hover, Location, Position, Range } from 'vscode-languageserver'; +import type { CodeAction, CompletionItem, Hover, Location, Position, Range } from 'vscode-languageserver'; +import { CodeActionKind } from 'vscode-languageserver'; import { DiagnosticMessages } from '../DiagnosticMessages'; import type { FunctionScope } from '../FunctionScope'; import type { Callable, BsDiagnostic, File, FileReference, FunctionCall } from '../interfaces'; @@ -369,6 +370,38 @@ export class XmlFile { return null; } + public getCodeActions(range: Range) { + const result = [] as CodeAction[]; + + for (const diagnostic of this.diagnostics) { + //skip diagnostics that don't occur on this line + if (diagnostic.range?.start.line !== range.start.line) { + continue; + } + if (diagnostic.code === DiagnosticMessages.xmlComponentMissingExtendsAttribute().code) { + //add the attribute at the end of the first attribute, or after the `` token + range.end.character -= 1; + result.push( + util.createCodeAction({ + title: `Add 'extends="Group"' attribute`, + // diagnostics: [diagnostic], + isPreferred: true, + kind: CodeActionKind.QuickFix, + changes: [{ + type: 'insert', + filePath: this.pathAbsolute, + position: util.createPosition(pos.line, pos.character), + newText: ' extends="Group"' + }] + }) + ); + } + } + return result; + } + public getReferences(position: Position): Promise { //eslint-disable-line //TODO implement return null; diff --git a/src/interfaces.ts b/src/interfaces.ts index 56cd05fc9..e61826e27 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -1,4 +1,4 @@ -import type { Range, Diagnostic } from 'vscode-languageserver'; +import type { Range, Diagnostic, CodeAction, Position, CodeActionKind } from 'vscode-languageserver'; import type { Scope } from './Scope'; import type { BrsFile } from './files/BrsFile'; import type { XmlFile } from './files/XmlFile'; @@ -187,6 +187,8 @@ export interface CompilerPlugin { afterFileTranspile?: (entry: TranspileObj) => void; beforeFileDispose?: (file: BscFile) => void; afterFileDispose?: (file: BscFile) => void; + beforeGetCodeActions?: (file: BscFile, range: Range, codeActions: CodeAction[]) => void; + afterGetCodeActions?: (file: BscFile, range: Range, codeActions: CodeAction[]) => void; } export interface TypedefProvider { @@ -202,3 +204,25 @@ export interface ExpressionInfo { varExpressions: Expression[]; uniqueVarNames: string[]; } + +export interface CodeActionShorthand { + title: string; + diagnostics?: Diagnostic[]; + kind?: CodeActionKind; + isPreferred?: boolean; + changes: Array; +} + +export interface InsertChange { + filePath: string; + newText: string; + type: 'insert'; + position: Position; +} + +export interface ReplaceChange { + filePath: string; + newText: string; + type: 'replace'; + range: Range; +} diff --git a/src/util.ts b/src/util.ts index 148098ea0..0a0b94109 100644 --- a/src/util.ts +++ b/src/util.ts @@ -4,12 +4,12 @@ import type { ParseError } from 'jsonc-parser'; import { parse as parseJsonc, printParseErrorCode } from 'jsonc-parser'; import * as path from 'path'; import * as rokuDeploy from 'roku-deploy'; -import type { Position, Range } from 'vscode-languageserver'; +import { CodeAction, Position, Range, TextEdit, WorkspaceEdit } from 'vscode-languageserver'; import { URI } from 'vscode-uri'; import * as xml2js from 'xml2js'; import type { BsConfig } from './BsConfig'; import { DiagnosticMessages } from './DiagnosticMessages'; -import type { CallableContainer, BsDiagnostic, FileReference, CallableContainerMap, CompilerPluginFactory, CompilerPlugin, ExpressionInfo } from './interfaces'; +import type { CallableContainer, BsDiagnostic, FileReference, CallableContainerMap, CompilerPluginFactory, CompilerPlugin, ExpressionInfo, CodeActionShorthand } from './interfaces'; import { BooleanType } from './types/BooleanType'; import { DoubleType } from './types/DoubleType'; import { DynamicType } from './types/DynamicType'; @@ -1175,6 +1175,28 @@ export class Util { range: attr.range } as SGAttribute; } + + 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); + } } /** From 656e2ce663489018441f55e87cf3c5507e5321b6 Mon Sep 17 00:00:00 2001 From: Bronley Date: Tue, 2 Feb 2021 17:09:03 -0500 Subject: [PATCH 02/20] better code action message. --- src/files/XmlFile.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/files/XmlFile.ts b/src/files/XmlFile.ts index acf1f2a5e..e3939a1cc 100644 --- a/src/files/XmlFile.ts +++ b/src/files/XmlFile.ts @@ -385,7 +385,7 @@ export class XmlFile { range.end.character -= 1; result.push( util.createCodeAction({ - title: `Add 'extends="Group"' attribute`, + title: `Add default extends attribute`, // diagnostics: [diagnostic], isPreferred: true, kind: CodeActionKind.QuickFix, From 81daaf4a499ab6ac0758073e8e9ef768849398e5 Mon Sep 17 00:00:00 2001 From: Bronley Date: Tue, 2 Feb 2021 23:24:08 -0500 Subject: [PATCH 03/20] Add import script tag codeAction --- src/DiagnosticMessages.ts | 9 ++++--- src/Program.ts | 12 ++++++++- src/Scope.ts | 7 +++++- src/XmlScope.spec.ts | 42 ++++++++++++++++++++++++++++++++ src/XmlScope.ts | 46 ++++++++++++++++++++++++++++++++--- src/parser/Parser.ts | 51 +++++++++++++++++++-------------------- src/parser/SGParser.ts | 2 +- src/parser/SGTypes.ts | 2 ++ src/util.ts | 3 ++- 9 files changed, 137 insertions(+), 37 deletions(-) diff --git a/src/DiagnosticMessages.ts b/src/DiagnosticMessages.ts index 4857b3d67..69c119d77 100644 --- a/src/DiagnosticMessages.ts +++ b/src/DiagnosticMessages.ts @@ -17,6 +17,7 @@ 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, + functionName: name, severity: DiagnosticSeverity.Error }), mismatchArgumentCount: (expectedCount: number | string, actualCount: number) => ({ @@ -608,13 +609,13 @@ export let DiagnosticMessages = { }) }; -let allCodes = [] as number[]; +export const DiagnosticCodeMap = {} as Record; +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; diff --git a/src/Program.ts b/src/Program.ts index 35574b545..8edb92f6a 100644 --- a/src/Program.ts +++ b/src/Program.ts @@ -747,7 +747,17 @@ export class Program { ...file.getCodeActions(range) ); - //TODO get code actions from each scope + //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 + codeActions.push( + ...scope.getCodeActions(file, range) + ); + } + } this.plugins.emit('beforeGetCodeActions', file, range, codeActions); return codeActions; diff --git a/src/Scope.ts b/src/Scope.ts index 80da7e3fc..e7997c895 100644 --- a/src/Scope.ts +++ b/src/Scope.ts @@ -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'; @@ -243,6 +243,11 @@ export class Scope { this.diagnostics.push(...diagnostics); } + public getCodeActions(file: BscFile, range: Range) { + const result = [] as CodeAction[]; + return result; + } + /** * Get the list of callables available in this scope (either declared in this scope or in a parent scope) */ diff --git a/src/XmlScope.spec.ts b/src/XmlScope.spec.ts index 248c45d24..fdbb9a520 100644 --- a/src/XmlScope.spec.ts +++ b/src/XmlScope.spec.ts @@ -6,6 +6,8 @@ import { Program } from './Program'; import { trim } from './testHelpers.spec'; import { standardizePath as s, util } from './util'; let rootDir = s`${process.cwd()}/rootDir`; +import { createSandbox } from 'sinon'; +const sinon = createSandbox(); describe('XmlScope', () => { let program: Program; @@ -13,10 +15,12 @@ describe('XmlScope', () => { program = new Program({ rootDir: rootDir }); + sinon.restore(); }); afterEach(() => { program.dispose(); + sinon.restore(); }); describe('constructor', () => { @@ -187,4 +191,42 @@ describe('XmlScope', () => { }); }); }); + + describe('getCodeActions', () => { + it('sugests import script tag for function from not-imported file', () => { + program.addOrReplaceFile('components/comp1.xml', trim` + + +