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

Feat/enum completion #485

Merged
merged 25 commits into from
Feb 1, 2022
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
f7dd5bd
adds completions for enum types
georgejecook Jan 15, 2022
a657ccc
add tests to prove there are no enum member collisions
georgejecook Jan 15, 2022
2ce9139
Merge branch 'feat/enum' into feat/enum-completion
georgejecook Jan 15, 2022
d00fae9
Formatting fixes
TwitchBronBron Jan 16, 2022
8a11418
small var naming tweaks
TwitchBronBron Jan 16, 2022
4f98afd
remove unnecessary eslint disable.
TwitchBronBron Jan 16, 2022
73a626b
clean up test a little
TwitchBronBron Jan 16, 2022
5fd98b8
adds validation for enums
georgejecook Jan 16, 2022
6d208c5
Merge branch 'feat/enum-completion' of github.com:rokucommunity/brigh…
georgejecook Jan 16, 2022
9f6bdec
Merge branch 'feat/enum-completion' into feat/enum-validation
georgejecook Jan 16, 2022
4120166
adds completions for enum types
georgejecook Jan 15, 2022
1c9a819
add tests to prove there are no enum member collisions
georgejecook Jan 15, 2022
3e5459c
Formatting fixes
TwitchBronBron Jan 16, 2022
cd84ac8
small var naming tweaks
TwitchBronBron Jan 16, 2022
7b43c03
remove unnecessary eslint disable.
TwitchBronBron Jan 16, 2022
bf76c71
clean up test a little
TwitchBronBron Jan 16, 2022
6a98859
changes namespace.enumStatements to map
georgejecook Jan 16, 2022
775bcdd
Merge branch 'feat/enum' into feat/enum-completion
georgejecook Jan 16, 2022
35bf2ac
changes namespace.enumStatements to map
georgejecook Jan 16, 2022
333e0ab
Merge branch 'feat/enum-validation' into feat/enum-completion
georgejecook Jan 16, 2022
e40710c
fixes broken imports
georgejecook Jan 16, 2022
83495db
uses visitor to ascertain dotted get statements
georgejecook Jan 16, 2022
2619720
fix circular ref issues.
TwitchBronBron Jan 16, 2022
db76662
Merge branch 'feat/enum' of https://github.com/rokucommunity/brighter…
TwitchBronBron Jan 25, 2022
996836e
clean up some tests
TwitchBronBron Feb 1, 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
1 change: 0 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

30 changes: 28 additions & 2 deletions src/Scope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { DiagnosticMessages } from './DiagnosticMessages';
import type { CallableContainer, BsDiagnostic, FileReference, BscFile, CallableContainerMap } from './interfaces';
import type { FileLink, Program } from './Program';
import { BsClassValidator } from './validators/ClassValidator';
import type { NamespaceStatement, Statement, NewExpression, FunctionStatement, ClassStatement } from './parser';
import type { NamespaceStatement, Statement, NewExpression, FunctionStatement, ClassStatement, EnumStatement } from './parser';
import { ParseMode } from './parser';
import { standardizePath as s, util } from './util';
import { globalCallableMap } from './globalCallables';
Expand Down Expand Up @@ -112,6 +112,28 @@ export class Scope {
});
}

/**
* A dictionary of all enums in this scope. This includes namespaced enums always with their full name.
* The key is stored in lower case
*/
public getEnumMap(): Map<string, FileLink<EnumStatement>> {
return this.cache.getOrAdd('enumMap', () => {
const map = new Map<string, FileLink<EnumStatement>>();
this.enumerateBrsFiles((file) => {
if (isBrsFile(file)) {
for (let enumStmt of file.parser.references.enumStatements) {
const lowerEnumName = enumStmt.fullName.toLowerCase();
//only track enums with a defined name (i.e. exclude nameless malformed enums)
if (lowerEnumName) {
map.set(lowerEnumName, { item: enumStmt, file: file });
}
}
}
});
return map;
});
}

/**
* The list of diagnostics found specifically for this scope. Individual file diagnostics are stored on the files themselves.
*/
Expand Down Expand Up @@ -359,7 +381,8 @@ export class Scope {
namespaces: new Map<string, NamespaceContainer>(),
classStatements: {},
functionStatements: {},
statements: []
statements: [],
enumStatements: {}
});
}
}
Expand All @@ -370,6 +393,8 @@ export class Scope {
ns.classStatements[statement.name.text.toLowerCase()] = statement;
} else if (isFunctionStatement(statement) && statement.name) {
ns.functionStatements[statement.name.text.toLowerCase()] = statement;
} else if (isFunctionStatement(statement) && statement.name) {
TwitchBronBron marked this conversation as resolved.
Show resolved Hide resolved
ns.functionStatements[statement.name.text.toLowerCase()] = statement;
}
}
}
Expand Down Expand Up @@ -1010,6 +1035,7 @@ interface NamespaceContainer {
statements: Statement[];
classStatements: Record<string, ClassStatement>;
functionStatements: Record<string, FunctionStatement>;
enumStatements: Record<string, EnumStatement>;
TwitchBronBron marked this conversation as resolved.
Show resolved Hide resolved
namespaces: Map<string, NamespaceContainer>;
}

Expand Down
105 changes: 105 additions & 0 deletions src/files/BrsFile.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,111 @@ describe('BrsFile', () => {
expect(names).to.includes('SayHello');
});

describe('namespaces', () => {

it('gets namespace completions', () => {
program.addOrReplaceFile('source/main.bs', `
namespace foo.bar
function sayHello()
end function
end namespace

sub Main()
print "hello"
foo.ba
foo.bar.
end sub
`);

let result = program.getCompletions(`${rootDir}/source/main.bs`, Position.create(8, 30));
let names = result.map(x => x.label);
expect(names).to.includes('bar');

result = program.getCompletions(`${rootDir}/source/main.bs`, Position.create(9, 32));
names = result.map(x => x.label);
expect(names).to.includes('sayHello');
});
});

describe('enums', () => {
it('gets enum completions', () => {
program.addOrReplaceFile('source/main.bs', `
enum foo
bar1
bar2
end enum

sub Main()
print "hello"
foo.ba
test.foo2.ba
end sub

namespace test
function fooFace2()
end function
class fooClass2
end class

enum foo2
bar2_1
bar2_2
end enum
end namespace
function fooFace()
end function
class fooClass
end class
enum foo3
bar3_1
bar3_2
end enum
`);

let result;
let names;
result = program.getCompletions(`${rootDir}/source/main.bs`, Position.create(8, 26));
names = result.map(x => x.label);
expect(names).to.includes('foo');
expect(names).to.includes('foo3');
expect(names).to.includes('fooFace');
expect(names).to.includes('fooClass');
expect(result[2].kind).to.equal(CompletionItemKind.Enum);
expect(result[3].kind).to.equal(CompletionItemKind.Enum);


result = program.getCompletions(`${rootDir}/source/main.bs`, Position.create(8, 27));
names = result.map(x => x.label);
expect(names).to.includes('foo');
expect(names).to.includes('foo3');
expect(names).to.includes('fooFace');
expect(names).to.includes('fooClass');
expect(result[2].kind).to.equal(CompletionItemKind.Enum);
expect(result[3].kind).to.equal(CompletionItemKind.Enum);

result = program.getCompletions(`${rootDir}/source/main.bs`, Position.create(8, 30));
names = result.map(x => x.label);
expect(names).to.includes('bar1');
expect(names).to.includes('bar2');
expect(result[0].kind).to.equal(CompletionItemKind.EnumMember);
expect(result[1].kind).to.equal(CompletionItemKind.EnumMember);

result = program.getCompletions(`${rootDir}/source/main.bs`, Position.create(9, 33));
names = result.map(x => x.label);
expect(names).to.includes('foo2');
expect(names).to.includes('fooFace2');
expect(names).to.includes('fooClass2');
expect(result[2].kind).to.equal(CompletionItemKind.Enum);

result = program.getCompletions(`${rootDir}/source/main.bs`, Position.create(9, 36));
names = result.map(x => x.label);
expect(names).to.includes('bar2_1');
expect(names).to.includes('bar2_2');
expect(result[0].kind).to.equal(CompletionItemKind.EnumMember);
expect(result[1].kind).to.equal(CompletionItemKind.EnumMember);
});

});
it('always includes `m`', () => {
//eslint-disable-next-line @typescript-eslint/no-floating-promises
program.addOrReplaceFile({ src: `${rootDir}/source/main.brs`, dest: 'source/main.brs' }, `
Expand Down
91 changes: 84 additions & 7 deletions src/files/BrsFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { BrsTranspileState } from '../parser/BrsTranspileState';
import { Preprocessor } from '../preprocessor/Preprocessor';
import { LogLevel } from '../Logger';
import { serializeError } from 'serialize-error';
import { isCallExpression, isClassMethodStatement, isClassStatement, isCommentStatement, isDottedGetExpression, isFunctionExpression, isFunctionStatement, isFunctionType, isLibraryStatement, isLiteralExpression, isNamespaceStatement, isStringType, isVariableExpression, isXmlFile, isImportStatement, isClassFieldStatement } from '../astUtils/reflection';
import { isCallExpression, isClassMethodStatement, isClassStatement, isCommentStatement, isDottedGetExpression, isFunctionExpression, isFunctionStatement, isFunctionType, isLibraryStatement, isLiteralExpression, isNamespaceStatement, isStringType, isVariableExpression, isXmlFile, isImportStatement, isClassFieldStatement, isEnumStatement } from '../astUtils/reflection';
import type { BscType } from '../types/BscType';
import { createVisitor, WalkMode } from '../astUtils/visitors';
import type { DependencyGraph } from '../DependencyGraph';
Expand Down Expand Up @@ -751,9 +751,17 @@ export class BrsFile {
}
}

//temporary workaround - the enums are not appearing on namspace, so we have to look them up first
let enumCompletions = this.getEnumStatementCompletions(currentToken, this.parseMode);
let namespaceCompletions = this.getNamespaceCompletions(currentToken, this.parseMode, scope);
if (namespaceCompletions.length > 0) {
return namespaceCompletions;
return [...namespaceCompletions, ...enumCompletions];
}

let enumMemberCompletions = this.getEnumMemberStatementCompletions(currentToken, this.parseMode);
if (enumMemberCompletions.length > 0) {
// no other completion is valid, in this case
return enumMemberCompletions;
}
//determine if cursor is inside a function
let functionScope = this.getFunctionScopeAtPosition(position);
Expand All @@ -763,7 +771,7 @@ export class BrsFile {
// there's a new keyword, so only class types are viable here
return [...this.getGlobalClassStatementCompletions(currentToken, this.parseMode)];
} else {
return [...KeywordCompletions, ...this.getGlobalClassStatementCompletions(currentToken, this.parseMode), ...namespaceCompletions];
return [...KeywordCompletions, ...this.getGlobalClassStatementCompletions(currentToken, this.parseMode), ...namespaceCompletions, ...this.getEnumStatementCompletions(currentToken, this.parseMode)];
}
}

Expand All @@ -781,10 +789,6 @@ export class BrsFile {
}

if (this.isPositionNextToTokenKind(position, TokenKind.Dot)) {
if (namespaceCompletions.length > 0) {
//if we matched a namespace, after a dot, it can't be anything else but something from our namespace completions
return namespaceCompletions;
}

const selfClassMemberCompletions = this.getClassMemberCompletions(position, currentToken, functionScope, scope);

Expand All @@ -807,6 +811,9 @@ export class BrsFile {
//include class names
result.push(...classNameCompletions);

//include enums
result.push(...enumCompletions);

//include the global callables
result.push(...scope.getCallablesAsCompletions(this.parseMode));

Expand Down Expand Up @@ -917,6 +924,61 @@ export class BrsFile {
return [...results.values()];
}

private getEnumStatementCompletions(currentToken: Token, parseMode: ParseMode): CompletionItem[] {
if (parseMode === ParseMode.BrightScript) {
return [];
}
let results = new Map<string, CompletionItem>();
let completionName = this.getPartialVariableName(currentToken)?.toLowerCase();
let scopes = this.program.getScopesForFile(this);
for (let scope of scopes) {
let enumMap = scope.getEnumMap();
for (const key of [...enumMap.keys()]) {
let es = enumMap.get(key).item;
if (es.fullName.startsWith(completionName)) {

if (!results.has(es.fullName)) {
results.set(es.fullName, {
label: es.name,
kind: CompletionItemKind.Enum
});
}
}
}
}
return [...results.values()];
}
private getEnumMemberStatementCompletions(currentToken: Token, parseMode: ParseMode): CompletionItem[] {
if (parseMode === ParseMode.BrightScript) {
return [];
}
let results = new Map<string, CompletionItem>();
let completionName = this.getPartialVariableName(currentToken)?.toLowerCase();
let scopes = this.program.getScopesForFile(this);
for (let scope of scopes) {
let enumMap = scope.getEnumMap();
for (const key of [...enumMap.keys()]) {
let enumStmt = enumMap.get(key).item;
if (completionName.startsWith(enumStmt.fullName) && completionName.length > enumStmt.fullName.length) {

for (const member of enumStmt.getMembers()) {
const name = enumStmt.fullName + '.' + member.name;
if (name.startsWith(completionName)) {
if (!results.has(name)) {
results.set(name, {
label: member.name,
kind: CompletionItemKind.EnumMember
});

}
}
}
}
}
}
return [...results.values()];
}

private getNamespaceCompletions(currentToken: Token, parseMode: ParseMode, scope: Scope): CompletionItem[] {
//BrightScript does not support namespaces, so return an empty list in that case
if (parseMode === ParseMode.BrightScript) {
Expand Down Expand Up @@ -968,7 +1030,22 @@ export class BrsFile {
kind: CompletionItemKind.Function
});
}
} else if (isEnumStatement(stmt) && !newToken) {
if (!result.has(stmt.name)) {
result.set(stmt.name, {
label: stmt.name,
kind: CompletionItemKind.Enum
});
}
}
// else if (isEnumMemberStatement(stmt) && !newToken) {
// if (!result.has(stmt.name)) {
// result.set(stmt.name, {
// label: stmt.name,
// kind: CompletionItemKind.EnumMember
// });
// }
// }

}

Expand Down
4 changes: 2 additions & 2 deletions src/parser/Statement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2208,14 +2208,14 @@ export class EnumStatement extends Statement implements TypedefProvider {
}

/**
* The name of the interface (without the namespace prefix)
* The name of the enum (without the namespace prefix)
*/
public get name() {
return this.tokens.name?.text;
}

/**
* The name of the interface WITH its leading namespace (if applicable)
* The name of the enum WITH its leading namespace (if applicable)
*/
public get fullName() {
const name = this.tokens.name?.text;
Expand Down