From a06532f83b6957b812e9bf421ef3bc011f088987 Mon Sep 17 00:00:00 2001 From: Bronley Date: Fri, 9 Apr 2021 15:01:53 -0400 Subject: [PATCH 1/3] Fix `replace` code action, add delete support. --- src/CodeActionUtil.ts | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/CodeActionUtil.ts b/src/CodeActionUtil.ts index 76b4b71ee..35ca6edf1 100644 --- a/src/CodeActionUtil.ts +++ b/src/CodeActionUtil.ts @@ -20,7 +20,11 @@ export class CodeActionUtil { TextEdit.insert(change.position, change.newText) ); } else if (change.type === 'replace') { - TextEdit.replace(change.range, change.newText); + edit.changes[uri].push( + TextEdit.replace(change.range, change.newText) + ); + } else if (change.type === 'delete') { + TextEdit.del(change.range); } } return CodeAction.create(obj.title, edit); @@ -32,7 +36,7 @@ export interface CodeActionShorthand { diagnostics?: Diagnostic[]; kind?: CodeActionKind; isPreferred?: boolean; - changes: Array; + changes: Array; } export interface InsertChange { @@ -49,4 +53,10 @@ export interface ReplaceChange { range: Range; } +export interface DeleteChange { + filePath: string; + type: 'delete'; + range: Range; +} + export const codeActionUtil = new CodeActionUtil(); From dcbc3a8ad6a8109679b304616d8ad8a8891da179 Mon Sep 17 00:00:00 2001 From: Bronley Date: Fri, 9 Apr 2021 15:02:46 -0400 Subject: [PATCH 2/3] Add callfunc refactor code action --- .../codeActions/CodeActionsProcessor.spec.ts | 45 +++++++++++++++ .../codeActions/CodeActionsProcessor.ts | 57 ++++++++++++++++++- src/files/BrsFile.ts | 15 +++++ 3 files changed, 116 insertions(+), 1 deletion(-) diff --git a/src/bscPlugin/codeActions/CodeActionsProcessor.spec.ts b/src/bscPlugin/codeActions/CodeActionsProcessor.spec.ts index a1353a990..acd978064 100644 --- a/src/bscPlugin/codeActions/CodeActionsProcessor.spec.ts +++ b/src/bscPlugin/codeActions/CodeActionsProcessor.spec.ts @@ -225,6 +225,51 @@ describe('CodeActionsProcessor', () => { `import "pkg:/source/Animals.bs"` ]); }); + + it('suggests callfunc operator refactor', () => { + //define the function in two files + const file = program.addOrReplaceFile('components/main.bs', ` + sub doSomething() + someComponent.callfunc("doThing") + someComponent.callfunc("doOtherThing", 1, 2) + end sub + `); + program.validate(); + + expectCodeActions(() => { + program.getCodeActions( + file.pathAbsolute, + util.createRange(2, 39, 2, 39) + ); + }, [{ + title: `Refactor to use callfunc operator`, + isPreferred: false, + kind: 'quickfix', + changes: [{ + filePath: file.pathAbsolute, + newText: '@.doThing(', + type: 'replace', + range: util.createRange(2, 33, 2, 52) + }] + }]); + + expectCodeActions(() => { + program.getCodeActions( + file.pathAbsolute, + util.createRange(3, 39, 3, 39) + ); + }, [{ + title: `Refactor to use callfunc operator`, + isPreferred: false, + kind: 'quickfix', + changes: [{ + filePath: file.pathAbsolute, + newText: '@.doOtherThing(', + type: 'replace', + range: util.createRange(3, 33, 3, 59) + }] + }]); + }); }); }); diff --git a/src/bscPlugin/codeActions/CodeActionsProcessor.ts b/src/bscPlugin/codeActions/CodeActionsProcessor.ts index 8de1e3b77..06d7a9fdc 100644 --- a/src/bscPlugin/codeActions/CodeActionsProcessor.ts +++ b/src/bscPlugin/codeActions/CodeActionsProcessor.ts @@ -1,11 +1,14 @@ -import type { Diagnostic } from 'vscode-languageserver'; +import type { Diagnostic, Position } from 'vscode-languageserver'; import { CodeActionKind } from 'vscode-languageserver'; +import { isBrsFile } from '../../astUtils'; import { codeActionUtil } from '../../CodeActionUtil'; import type { DiagnosticMessageType } from '../../DiagnosticMessages'; import { DiagnosticCodeMap } from '../../DiagnosticMessages'; import type { BrsFile } from '../../files/BrsFile'; import type { XmlFile } from '../../files/XmlFile'; import type { BscFile, OnGetCodeActionsEvent } from '../../interfaces'; +import type { Token } from '../../lexer/Token'; +import { TokenKind } from '../../lexer/TokenKind'; import { ParseMode } from '../../parser'; import { util } from '../../util'; @@ -26,6 +29,58 @@ export class CodeActionsProcessor { this.addMissingExtends(diagnostic as any); } } + + if (isBrsFile(this.event.file)) { + const token = this.event.file.getTokenAt(this.event.range.start); + const previousToken = this.event.file.getPreviousToken(token); + const lowerText = token?.text?.toLowerCase(); + + //brighterscript-specific code actions + if (this.event.file.extension === '.bs') { + if (lowerText === 'callfunc' && previousToken.kind === TokenKind.Dot) { + this.suggestCallfuncOperator(token, previousToken); + } + } + } + } + + /** + * Suggest refactoring callfunc calls to use the callfunc operator instead + */ + private suggestCallfuncOperator(token: Token, previousToken: Token) { + const file = this.event.file as BrsFile; + const openParenToken = file.getNextToken(token); + const functionNameToken = file.getNextToken(openParenToken); + + const comma = file.getNextToken(functionNameToken, TokenKind.Comma); + let endPosition: Position; + //consume the comma and any space up until the next token + if (comma) { + const tokenAfterComma = file.getNextToken(comma); + endPosition = tokenAfterComma?.range.start ?? comma?.range.end; + } else { + endPosition = functionNameToken.range.end; + } + + + //if we have the necessary tokens + if (openParenToken?.kind === TokenKind.LeftParen && functionNameToken?.kind === TokenKind.StringLiteral) { + //replace leading and trailing quotes + const functionName = functionNameToken.text.replace(/^"/, '').replace(/"$/, ''); + this.event.codeActions.push( + codeActionUtil.createCodeAction({ + title: `Refactor to use callfunc operator`, + isPreferred: false, + kind: CodeActionKind.QuickFix, + changes: [{ + type: 'replace', + filePath: this.event.file.pathAbsolute, + newText: `@.${functionName}(`, + range: util.createRangeFromPositions(previousToken.range.start, endPosition) + }] + }) + ); + } } private suggestedImports = new Set(); diff --git a/src/files/BrsFile.ts b/src/files/BrsFile.ts index 68a4383d4..f819b960c 100644 --- a/src/files/BrsFile.ts +++ b/src/files/BrsFile.ts @@ -1092,6 +1092,21 @@ export class BrsFile { return parser.tokens[idx - 1]; } + /** + * Get the token after the given token. + * If `requiredTokenKind` is specified, then the next token's type is checked, and if no match, undefined is returned. + */ + public getNextToken(token: Token, requiredTokenKind?: TokenKind) { + const parser = this.parser; + let idx = parser.tokens.indexOf(token); + const result = parser.tokens[idx + 1]; + if (!requiredTokenKind) { + return result; + } else if (result?.kind === requiredTokenKind) { + return result; + } + } + /** * Find the first scope that has a namespace with this name. * Returns false if no namespace was found with that name From 5584b22003e6a8809267ad51462822e08fca28d5 Mon Sep 17 00:00:00 2001 From: Bronley Date: Fri, 9 Apr 2021 17:19:43 -0400 Subject: [PATCH 3/3] refactor invalid as first callfunc argument. --- .../codeActions/CodeActionsProcessor.spec.ts | 43 ++++++++++++++++--- .../codeActions/CodeActionsProcessor.ts | 9 ++-- 2 files changed, 43 insertions(+), 9 deletions(-) diff --git a/src/bscPlugin/codeActions/CodeActionsProcessor.spec.ts b/src/bscPlugin/codeActions/CodeActionsProcessor.spec.ts index acd978064..17ad79a43 100644 --- a/src/bscPlugin/codeActions/CodeActionsProcessor.spec.ts +++ b/src/bscPlugin/codeActions/CodeActionsProcessor.spec.ts @@ -226,12 +226,10 @@ describe('CodeActionsProcessor', () => { ]); }); - it('suggests callfunc operator refactor', () => { - //define the function in two files + it('suggests callfunc operator refactor (parameterless)', () => { const file = program.addOrReplaceFile('components/main.bs', ` sub doSomething() someComponent.callfunc("doThing") - someComponent.callfunc("doOtherThing", 1, 2) end sub `); program.validate(); @@ -252,11 +250,20 @@ describe('CodeActionsProcessor', () => { range: util.createRange(2, 33, 2, 52) }] }]); + }); + + it('suggests callfunc operator refactor (parameters)', () => { + const file = program.addOrReplaceFile('components/main.bs', ` + sub doSomething() + someComponent.callfunc("doOtherThing", someIdentifier, 2) + end sub + `); + program.validate(); expectCodeActions(() => { program.getCodeActions( file.pathAbsolute, - util.createRange(3, 39, 3, 39) + util.createRange(2, 39, 2, 39) ); }, [{ title: `Refactor to use callfunc operator`, @@ -266,7 +273,33 @@ describe('CodeActionsProcessor', () => { filePath: file.pathAbsolute, newText: '@.doOtherThing(', type: 'replace', - range: util.createRange(3, 33, 3, 59) + range: util.createRange(2, 33, 2, 59) + }] + }]); + }); + + it('suggests callfunc operator refactor (remove "invalid" first param)', () => { + const file = program.addOrReplaceFile('components/main.bs', ` + sub doSomething() + someComponent.callfunc("doYetAnotherThing", invalid) + end sub + `); + program.validate(); + + expectCodeActions(() => { + program.getCodeActions( + file.pathAbsolute, + util.createRange(2, 39, 2, 39) + ); + }, [{ + title: `Refactor to use callfunc operator`, + isPreferred: false, + kind: 'quickfix', + changes: [{ + filePath: file.pathAbsolute, + newText: '@.doYetAnotherThing(', + type: 'replace', + range: util.createRange(2, 33, 2, 71) }] }]); }); diff --git a/src/bscPlugin/codeActions/CodeActionsProcessor.ts b/src/bscPlugin/codeActions/CodeActionsProcessor.ts index 06d7a9fdc..7af80f93b 100644 --- a/src/bscPlugin/codeActions/CodeActionsProcessor.ts +++ b/src/bscPlugin/codeActions/CodeActionsProcessor.ts @@ -53,16 +53,17 @@ export class CodeActionsProcessor { const functionNameToken = file.getNextToken(openParenToken); const comma = file.getNextToken(functionNameToken, TokenKind.Comma); + const invalidLiteral = file.getNextToken(comma, TokenKind.Invalid); let endPosition: Position; + const endToken = invalidLiteral ?? comma; //consume the comma and any space up until the next token - if (comma) { - const tokenAfterComma = file.getNextToken(comma); - endPosition = tokenAfterComma?.range.start ?? comma?.range.end; + if (endToken) { + const tokenAfter = file.getNextToken(endToken); + endPosition = tokenAfter?.range.start ?? comma?.range.end; } else { endPosition = functionNameToken.range.end; } - //if we have the necessary tokens if (openParenToken?.kind === TokenKind.LeftParen && functionNameToken?.kind === TokenKind.StringLiteral) { //replace leading and trailing quotes