Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions package-lock.json

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

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"icon": "images/vba-lsp-icon.png",
"author": "SSlinky",
"license": "MIT",
"version": "1.7.3",
"version": "1.7.4",
"repository": {
"type": "git",
"url": "https://github.com/SSlinky/VBA-LanguageServer"
Expand Down
189 changes: 80 additions & 109 deletions server/src/capabilities/capabilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@ import {
import { ParserRuleContext, TerminalNode } from 'antlr4ng';

// Project
import { SemanticToken, SemanticTokenModifiers, SemanticTokenTypes } from '../capabilities/semanticTokens';
import { Services } from '../injection/services';
import { FoldingRange, FoldingRangeKind } from '../capabilities/folding';
import { SemanticToken, SemanticTokenModifiers, SemanticTokenTypes } from '../capabilities/semanticTokens';
import { BaseRuleSyntaxElement, BaseIdentifyableSyntaxElement, BaseSyntaxElement, Context, HasSemanticTokenCapability } from '../project/elements/base';
import { AmbiguousNameDiagnostic, BaseDiagnostic, DuplicateDeclarationDiagnostic, ShadowDeclarationDiagnostic, SubOrFunctionNotDefinedDiagnostic, UnusedDiagnostic, VariableNotDefinedDiagnostic } from './diagnostics';
import { Services } from '../injection/services';
import { isPositionInsideRange } from '../utils/helpers';
import { isPositionInsideRange, isRangeInsideRange } from '../utils/helpers';


abstract class BaseCapability {
Expand Down Expand Up @@ -221,7 +221,6 @@ export class ScopeItemCapability {

// Technical
isDirty: boolean = true;
isInvalidated = false;

get maps() {
const result: Map<string, ScopeItemCapability[]>[] = [];
Expand Down Expand Up @@ -281,26 +280,13 @@ export class ScopeItemCapability {
public parent?: ScopeItemCapability,
) { }

clean(): void {
this.deleteInvalidatedScopes();
this.cleanInvalidatedLinks();
}

/**
* Recursively build from this node down.
*/
build(): void {
this.clean();

// Don't build self if invalidated.
if (this.isInvalidated) {
return;
}

if (this.type === ScopeType.REFERENCE) {
// Link to declaration if it exists.
this.resolveLinks();
const abc = 0;
if (!this.link) {
// TODO:
// References to variables should get a diagnostic if they aren't declared.
Expand Down Expand Up @@ -570,60 +556,6 @@ export class ScopeItemCapability {
linkItem.backlinks.push(this);
}

private deleteInvalidatedScopes() {
const removeInvalidatedScopes = (map: Map<string, ScopeItemCapability[]> | undefined) => {
if (!map) return;

const result = new Map<string, ScopeItemCapability[]>();
for (const [name, scopes] of map) {
const filteredScopes = scopes.filter(x => !x.isInvalidated);
if (filteredScopes.length > 0) {
result.set(name, filteredScopes);
}
}
if (result.size !== 0) {
return result;
}
};

this.types = removeInvalidatedScopes(this.types);
this.modules = removeInvalidatedScopes(this.modules);
this.functions = removeInvalidatedScopes(this.functions);
this.subroutines = removeInvalidatedScopes(this.subroutines);
if (this.properties) {
this.properties.getters = removeInvalidatedScopes(this.properties?.getters);
this.properties.setters = removeInvalidatedScopes(this.properties?.setters);
this.properties.letters = removeInvalidatedScopes(this.properties?.letters);
}
this.parameters = removeInvalidatedScopes(this.parameters);
this.references = removeInvalidatedScopes(this.references);
this.implicitDeclarations = removeInvalidatedScopes(this.implicitDeclarations);
}

private cleanInvalidatedLinks() {
const removeLinks = (map: Map<string, ScopeItemCapability[]> | undefined) => {
map?.forEach((scopes) => scopes.forEach(scope => {
if (scope.link && scope.link.isInvalidated) {
scope.link = undefined;
}
if (scope.backlinks) {
scope.backlinks = scope.backlinks.filter(link => !link.isInvalidated);
if (scope.backlinks.length === 0) scope.backlinks = undefined;
}
}));
};

removeLinks(this.types);
removeLinks(this.modules);
removeLinks(this.functions);
removeLinks(this.subroutines);
removeLinks(this.properties?.getters);
removeLinks(this.properties?.setters);
removeLinks(this.properties?.letters);
removeLinks(this.parameters);
removeLinks(this.references);
}

/** Returns the module this scope item falls under */
get module(): ScopeItemCapability | undefined {
if (this.type === ScopeType.MODULE || this.type === ScopeType.CLASS) {
Expand Down Expand Up @@ -660,30 +592,18 @@ export class ScopeItemCapability {
return false;
}

/** Get accessible declarations */
/** Get accessible declarations matching name. */
getAccessibleScopes(identifier: string, results: ScopeItemCapability[] = []): ScopeItemCapability[] {
// Add any non-public items we find at this level.
this.maps.forEach(map => {
map.get(identifier)?.forEach(item => {
if (!item.isPublicScope) {
results.push(item);
}
});
});

// Get all public scope types if we're at the project level.
if (this.type === ScopeType.PROJECT) {
this.modules?.forEach(modules => modules.forEach(
module => module.maps.forEach(map => {
map.get(identifier)?.forEach(item => {
if (item.isPublicScope) {
results.push(item);
}
});
})
));
this.types?.get(identifier)?.forEach(scope => results.push(scope));
this.modules?.get(identifier)?.forEach(scope => results.push(scope));
this.functions?.get(identifier)?.forEach(scope => results.push(scope));
this.subroutines?.get(identifier)?.forEach(scope => results.push(scope));
if (this.properties) {
this.properties.getters?.get(identifier)?.forEach(scope => results.push(scope));
this.properties.letters?.get(identifier)?.forEach(scope => results.push(scope));
this.properties.setters?.get(identifier)?.forEach(scope => results.push(scope));
}

this.parameters?.get(identifier)?.forEach(scope => results.push(scope));
return this.parent?.getAccessibleScopes(identifier, results) ?? results;
}

Expand Down Expand Up @@ -766,11 +686,6 @@ export class ScopeItemCapability {
* @returns The current scope.
*/
registerScopeItem(item: ScopeItemCapability): ScopeItemCapability {
// Immediately invalidate if we're an Unknown Module
if (item.type === ScopeType.MODULE && item.name === 'Unknown Module') {
item.isInvalidated = true;
}

// Set the parent for the item.
item.parent = this; // getParent(item);
item.parent.isDirty = true;
Expand Down Expand Up @@ -868,18 +783,74 @@ export class ScopeItemCapability {
return item;
}

invalidateModule(uri: string): void {
const module = this.findModuleByUri(uri);
module?.invalidate();
}
/**
* Recursively removes all scopes with the passed in uri and
* within the range bounds, including where it is linked.
*/
invalidate(uri: string, range: Range): void {
const isInvalidScope = (scope: ScopeItemCapability) =>
scope.locationUri === uri
&& scope.element?.context.range
&& isRangeInsideRange(scope.element.context.range, range);

const cleanScopes = (scopes?: ScopeItemCapability[]) => {
if (scopes === undefined) {
return undefined;
}

invalidate(): void {
this.isInvalidated = true;
this.maps.forEach(
map => map.forEach(
scopes => scopes.forEach(
scope => scope.invalidate()
)));
const result: ScopeItemCapability[] = [];
scopes.forEach(scope => {
if (isInvalidScope(scope)) {
Services.logger.debug(`Invalidating ${scope.name}`);

// Clean the backlinks on the linked item if we have one.
if (scope.link) scope.link.backlinks = cleanScopes(
scope.link.backlinks);

// Clean the invaludated scope.
scope.invalidate(uri, range);

return;
}
result.push(scope);
});
return result;
};

const cleanMap = (map?: Map<string, ScopeItemCapability[]>) => {
if (map === undefined) {
return undefined;
}

const result = new Map<string, ScopeItemCapability[]>();
for (const [name, scopes] of map) {
const cleanedScopes = cleanScopes(scopes);
if (cleanedScopes && cleanedScopes.length > 0) {
result.set(name, cleanedScopes);
}
}

if (result.size > 0) {
return result;
}
};

this.types = cleanMap(this.types);
this.modules = cleanMap(this.modules);
this.functions = cleanMap(this.functions);
this.subroutines = cleanMap(this.subroutines);
if (this.properties) {
this.properties.getters = cleanMap(this.properties.getters);
this.properties.letters = cleanMap(this.properties.letters);
this.properties.setters = cleanMap(this.properties.setters);
}
this.parameters = cleanMap(this.parameters);
this.implicitDeclarations = cleanMap(this.implicitDeclarations);

// Do a basic clean on backlinks that doesn't trigger recursion.
if (this.backlinks) {
this.backlinks = this.backlinks.filter(scope => !isInvalidScope(scope));
}
}

/** Returns true for public and false for private */
Expand Down
2 changes: 1 addition & 1 deletion server/src/injection/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export interface ILanguageServer {
export interface IWorkspace {
clearDocumentsConfiguration(): void
formatParseDocument(document: TextDocument, token: CancellationToken): Promise<VbaFmtListener | undefined>;
parseDocument(projectDocument: BaseProjectDocument): Promise<void>;
parseDocument(projectDocument: BaseProjectDocument, previousDocument?: BaseProjectDocument): Promise<void>;
openDocument(document: TextDocument): void;
closeDocument(document: TextDocument): void;
addWorkspaceFolder(params: WorkspaceFolder): void;
Expand Down
13 changes: 12 additions & 1 deletion server/src/project/document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,18 @@ export abstract class BaseProjectDocument {
return this.subtractTextFromRanges(this.redactedElements.map(x => x.context.range));
}

get uri(): string {
return this.textDocument.uri;
}

get range(): Range {
const size = this.textDocument.getText().length - 1;
return {
start: { line: 0, character: 0 },
end: this.textDocument.positionAt(size)
};
}

constructor(name: string, document: TextDocument) {
this.textDocument = document;
this.workspace = Services.workspace;
Expand Down Expand Up @@ -167,7 +179,6 @@ export abstract class BaseProjectDocument {
await (new SyntaxParser(Services.logger)).parse(token, this);
const projectScope = this.currentScope.project;
const buildScope = projectScope?.isDirty ? projectScope : this.currentScope;
projectScope?.clean();
buildScope.build();
buildScope.resolveUnused();

Expand Down
16 changes: 11 additions & 5 deletions server/src/project/workspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,11 +147,17 @@ export class Workspace implements IWorkspace {
// Services.projectScope.printToDebug();
}

async parseDocument(document: BaseProjectDocument) {
// this.activateDocument(document);
async parseDocument(document: BaseProjectDocument, previousDocument?: BaseProjectDocument) {
this.parseCancellationTokenSource?.cancel();
this.parseCancellationTokenSource = new CancellationTokenSource();

if (previousDocument) {
Services.projectScope.invalidate(
previousDocument.uri,
previousDocument.range
);
}

// Exceptions thrown by the parser should be ignored.
try {
await document.parse(this.parseCancellationTokenSource.token);
Expand Down Expand Up @@ -195,7 +201,8 @@ export class Workspace implements IWorkspace {
if (projectDocument) {
projectDocument.open();
if (document.version > projectDocument.version) {
this.parseDocument(projectDocument);
const newDocument = BaseProjectDocument.create(document);
this.parseDocument(newDocument, projectDocument);
}
this.connection.sendDiagnostics(projectDocument.languageServerDiagnostics());
}
Expand Down Expand Up @@ -576,8 +583,7 @@ class WorkspaceEvents {
// The document is new or a new version that we should parse.
const projectDocument = BaseProjectDocument.create(document);
this.projectDocuments.set(normalisedUri, projectDocument);
Services.projectScope.invalidateModule(normalisedUri);
Services.workspace.parseDocument(projectDocument);
Services.workspace.parseDocument(projectDocument, existingDocument);
}

/**
Expand Down
20 changes: 20 additions & 0 deletions server/src/utils/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,4 +87,24 @@ export function isPositionInsideRange(position: Position, range: Range): boolean
return position.line === range.start.line
&& position.character >= range.start.character
&& position.character <= range.end.character;
}

/**
* Returns true if the inner range is inside the outer range.
* @param inner The range to test as enveloped.
* @param outer The range to test as enveloping.
*/
export function isRangeInsideRange(inner: Range, outer: Range): boolean {
// Test characters on single-line ranges.
const isSingleLine = inner.start.line === inner.end.line
&& outer.start.line === outer.end.line
&& inner.start.line === outer.start.line;
if (isSingleLine) {
return inner.start.character >= outer.start.character
&& inner.end.character <= outer.end.character;
}

// Test lines on multi-line ranges.
return inner.start.line >= outer.start.line
&& inner.end.line <= outer.end.line;
}