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

Code action xml script import #488

Draft
wants to merge 27 commits into
base: master
Choose a base branch
from
Draft
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
302d700
Merge branch 'master' of https://github.com/rokucommunity/brighterscr…
TwitchBronBron Jan 18, 2022
2d36023
Fix all broken tests.
TwitchBronBron Jan 18, 2022
e32dfd8
Consolidate tests for code actions
TwitchBronBron Jan 18, 2022
6af8a16
remove unused util function
TwitchBronBron Jan 18, 2022
9e9769d
Fix incorrect line endings.
TwitchBronBron Jan 18, 2022
b753479
Merge branch 'codeAction-xml-import' of https://github.com/rokucommun…
TwitchBronBron Jan 18, 2022
63b5384
Merge branch 'master' of https://github.com/rokucommunity/brighterscr…
TwitchBronBron Apr 13, 2022
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
117 changes: 109 additions & 8 deletions src/bscPlugin/codeActions/CodeActionsProcessor.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,13 +152,16 @@ describe('CodeActionsProcessor', () => {

program.validate();

//there should be no code actions since this is a brs file
const codeActions = program.getCodeActions(
file.pathAbsolute,
// DoSometh|ing()
util.createRange(2, 28, 2, 28)
);
expect(codeActions).to.be.empty;
//the ImportStatement code action should be missing since this is a brs file
expect(
program.getCodeActions(
file.pathAbsolute,
// DoSometh|ing()
util.createRange(2, 28, 2, 28)
).map(x => x.title).sort()
).to.eql([
`Add xml script import "pkg:/source/lib.brs" into component "ChildScene"`
]);
});

it('suggests class imports', () => {
Expand Down Expand Up @@ -226,6 +229,104 @@ describe('CodeActionsProcessor', () => {
`import "pkg:/source/Animals.bs"`
]);
});
});

it('sugests import script tag for function from not-imported file', () => {
program.setFile('components/comp1.xml', trim`
<?xml version="1.0" encoding="utf-8" ?>
<component name="child">
<script uri="comp1.brs" />
</component>
`);
const codebehind = program.setFile('components/comp1.brs', `
sub init()
doSomething()
end sub
`);
program.setFile('source/common.brs', `
sub doSomething()
end sub
`);
program.validate();

expectCodeActions(() => {
program.getCodeActions(
codebehind.pathAbsolute,
// doSo|mething()
util.createRange(2, 24, 2, 24)
);
}, [{
title: `Add xml script import "pkg:/source/common.brs" into component "child"`,
changes: [{
filePath: s`${rootDir}/components/comp1.xml`,
newText: ' <script type="text/brightscript" uri="pkg:/source/common.brs" />\n',
type: 'insert',
position: util.createPosition(3, 0)
}]
}]);
});

it('suggests `extends=Group`', () => {
const file = program.setFile('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.setFile('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)
);
});
});
});
86 changes: 64 additions & 22 deletions src/bscPlugin/codeActions/CodeActionsProcessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ 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 { BscFile, BsDiagnostic, OnGetCodeActionsEvent } from '../../interfaces';
import { ParseMode } from '../../parser/Parser';
import { util } from '../../util';
import type { XmlScope } from '../../XmlScope';

export class CodeActionsProcessor {
public constructor(
Expand All @@ -19,9 +20,9 @@ export class CodeActionsProcessor {
public process() {
for (const diagnostic of this.event.diagnostics) {
if (diagnostic.code === DiagnosticCodeMap.callToUnknownFunction) {
this.suggestFunctionImports(diagnostic as any);
this.handleCallToUnknownFunction(diagnostic as any);
} else if (diagnostic.code === DiagnosticCodeMap.classCouldNotBeFound) {
this.suggestClassImports(diagnostic as any);
this.handleClassCouldNotBeFound(diagnostic as any);
} else if (diagnostic.code === DiagnosticCodeMap.xmlComponentMissingExtendsAttribute) {
this.addMissingExtends(diagnostic as any);
}
Expand All @@ -44,7 +45,7 @@ export class CodeActionsProcessor {
//find the position of the first import statement, or the top of the file if there is none
const insertPosition = importStatements[importStatements.length - 1]?.importToken.range?.start ?? util.createPosition(0, 0);

//find all files that reference this function
//suggest importing each file that references this item
for (const file of files) {
const pkgPath = util.getRokuPkgPath(file.pkgPath);
this.event.codeActions.push(
Expand All @@ -64,30 +65,71 @@ export class CodeActionsProcessor {
}
}

private suggestFunctionImports(diagnostic: DiagnosticMessageType<'callToUnknownFunction'>) {
//skip if not a BrighterScript file
if ((diagnostic.file as BrsFile).parseMode !== ParseMode.BrighterScript) {
/**
* Suggest a `<script>` reference for each of the specified files, for each of the scopes referencing the diagnostic's file.
* @param diagnostic
* @param key
* @param suggestedFiles a list of files suggested for import
*/
private suggestXmlScript(diagnostic: BsDiagnostic, key: string, suggestedFiles: BscFile[]) {
//skip if we already have this suggestion
if (this.suggestedImports.has(key)) {
return;
}
const lowerFunctionName = diagnostic.data.functionName.toLowerCase();
this.suggestImports(
diagnostic,
lowerFunctionName,
this.event.file.program.findFilesForFunction(lowerFunctionName)
);
this.suggestedImports.add(key);

//find all of the scopes that reference this diagnostic's file
for (const scope of this.event.scopes as XmlScope[]) {
//skip the global scope
if (scope.name === 'global') {
continue;
}
for (const file of suggestedFiles) {
const pkgPath = util.getRokuPkgPath(file.pkgPath);
const slashOpenToken = scope.xmlFile.parser.ast.component?.ast.SLASH_OPEN?.[0];
this.event.codeActions.push(
codeActionUtil.createCodeAction({
title: `Add xml script import "${pkgPath}" into component "${scope.xmlFile.componentName.text ?? scope.name}"`,
// diagnostics: [diagnostic]
changes: [{
filePath: scope.xmlFile.pathAbsolute,
newText: ` <script type="text/brightscript" uri="${pkgPath}" />\n`,
type: 'insert',
position: util.createPosition(slashOpenToken.startLine - 1, slashOpenToken.startColumn - 1)
}]
})
);
}
}
}

private suggestClassImports(diagnostic: DiagnosticMessageType<'classCouldNotBeFound'>) {
//skip if not a BrighterScript file
if ((diagnostic.file as BrsFile).parseMode !== ParseMode.BrighterScript) {
return;

private handleCallToUnknownFunction(diagnostic: DiagnosticMessageType<'callToUnknownFunction'>) {

const lowerFunctionName = diagnostic.data.functionName.toLowerCase();
const filesForFunction = this.event.file.program.findFilesForFunction(lowerFunctionName);

//suggest .bs `import` statements for brighterscript file
if ((diagnostic.file as BrsFile).parseMode === ParseMode.BrighterScript) {
this.suggestImports(diagnostic, lowerFunctionName, filesForFunction);
}

//suggest xml script tag imports
this.suggestXmlScript(diagnostic, lowerFunctionName, filesForFunction);
}

private handleClassCouldNotBeFound(diagnostic: DiagnosticMessageType<'classCouldNotBeFound'>) {
const lowerClassName = diagnostic.data.className.toLowerCase();
this.suggestImports(
diagnostic,
lowerClassName,
this.event.file.program.findFilesForClass(lowerClassName)
);

const filesForClass = this.event.file.program.findFilesForClass(lowerClassName);

//suggest .bs `import` statements for brighterscript file
if ((diagnostic.file as BrsFile).parseMode === ParseMode.BrighterScript) {
this.suggestImports(diagnostic, lowerClassName, filesForClass);
}

//suggest xml script tag imports
this.suggestXmlScript(diagnostic, lowerClassName, filesForClass);
}

private addMissingExtends(diagnostic: DiagnosticMessageType<'xmlComponentMissingExtendsAttribute'>) {
Expand Down
6 changes: 3 additions & 3 deletions src/interfaces.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Range, Diagnostic, CodeAction, SemanticTokenTypes, SemanticTokenModifiers } from 'vscode-languageserver';
import type { Range, Diagnostic, CodeAction, SemanticTokenModifiers, SemanticTokenTypes } from 'vscode-languageserver';
import type { Scope } from './Scope';
import type { BrsFile } from './files/BrsFile';
import type { XmlFile } from './files/XmlFile';
Expand Down Expand Up @@ -231,9 +231,9 @@ export interface CompilerPlugin {
}
export type PluginHandler<T> = (event: T) => void;

export interface OnGetCodeActionsEvent {
export interface OnGetCodeActionsEvent<TFile extends BscFile = BscFile> {
program: Program;
file: BscFile;
file: TFile;
range: Range;
scopes: Scope[];
diagnostics: BsDiagnostic[];
Expand Down
2 changes: 1 addition & 1 deletion src/parser/SGParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ function mapElement({ children }: ElementCstNode, diagnostics: Diagnostic[]): SG
switch (name.text) {
case 'component':
const componentContent = mapElements(content, ['interface', 'script', 'children', 'customization'], diagnostics);
return new SGComponent(name, attributes, componentContent, range);
return new SGComponent(children, name, attributes, componentContent, range);
case 'interface':
const interfaceContent = mapElements(content, ['field', 'function'], diagnostics);
return new SGInterface(name, interfaceContent, range);
Expand Down
2 changes: 2 additions & 0 deletions src/parser/SGTypes.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { ElementCtx } from '@xml-tools/parser';
import { SourceNode } from 'source-map';
import type { Range } from 'vscode-languageserver';
import { createSGAttribute } from '../astUtils/creators';
Expand Down Expand Up @@ -353,6 +354,7 @@ export class SGInterface extends SGTag {

export class SGComponent extends SGTag {
constructor(
public ast: ElementCtx,
tag: SGToken = { text: 'component' },
attributes?: SGAttribute[],
content?: SGTag[],
Expand Down