From 01487dbe01a57f84534b71b7350fa0276c37d15a Mon Sep 17 00:00:00 2001 From: sslinky <39886505+SSlinky@users.noreply.github.com> Date: Fri, 13 Jun 2025 14:57:55 +0800 Subject: [PATCH 1/4] New helper to check if a range is in a range --- server/src/utils/helpers.ts | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/server/src/utils/helpers.ts b/server/src/utils/helpers.ts index 40e69da..eaeb02a 100644 --- a/server/src/utils/helpers.ts +++ b/server/src/utils/helpers.ts @@ -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; } \ No newline at end of file From 828b26e7d859bcaa0eeb7dbdfd389a94a90f7ad2 Mon Sep 17 00:00:00 2001 From: sslinky <39886505+SSlinky@users.noreply.github.com> Date: Fri, 13 Jun 2025 14:59:56 +0800 Subject: [PATCH 2/4] New way to clean a project when reparsing --- server/src/capabilities/capabilities.ts | 178 +++++++++++------------- server/src/injection/interface.ts | 2 +- server/src/project/document.ts | 13 +- server/src/project/workspace.ts | 16 ++- 4 files changed, 105 insertions(+), 104 deletions(-) diff --git a/server/src/capabilities/capabilities.ts b/server/src/capabilities/capabilities.ts index 238a3b5..348f009 100644 --- a/server/src/capabilities/capabilities.ts +++ b/server/src/capabilities/capabilities.ts @@ -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 { @@ -221,7 +221,6 @@ export class ScopeItemCapability { // Technical isDirty: boolean = true; - isInvalidated = false; get maps() { const result: Map[] = []; @@ -281,22 +280,10 @@ 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(); @@ -570,60 +557,6 @@ export class ScopeItemCapability { linkItem.backlinks.push(this); } - private deleteInvalidatedScopes() { - const removeInvalidatedScopes = (map: Map | undefined) => { - if (!map) return; - - const result = new Map(); - 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 | 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) { @@ -672,17 +605,17 @@ export class ScopeItemCapability { }); // 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); - } - }); - }) - )); - } + // 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); + // } + // }); + // }) + // )); + // } return this.parent?.getAccessibleScopes(identifier, results) ?? results; } @@ -766,11 +699,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; @@ -868,18 +796,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) => { + if (map === undefined) { + return undefined; + } + + const result = new Map(); + 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 */ diff --git a/server/src/injection/interface.ts b/server/src/injection/interface.ts index 3ff1ce3..bbb798d 100644 --- a/server/src/injection/interface.ts +++ b/server/src/injection/interface.ts @@ -41,7 +41,7 @@ export interface ILanguageServer { export interface IWorkspace { clearDocumentsConfiguration(): void formatParseDocument(document: TextDocument, token: CancellationToken): Promise; - parseDocument(projectDocument: BaseProjectDocument): Promise; + parseDocument(projectDocument: BaseProjectDocument, previousDocument?: BaseProjectDocument): Promise; openDocument(document: TextDocument): void; closeDocument(document: TextDocument): void; addWorkspaceFolder(params: WorkspaceFolder): void; diff --git a/server/src/project/document.ts b/server/src/project/document.ts index 896c62c..5befb60 100644 --- a/server/src/project/document.ts +++ b/server/src/project/document.ts @@ -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; @@ -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(); diff --git a/server/src/project/workspace.ts b/server/src/project/workspace.ts index 35bab38..7884226 100644 --- a/server/src/project/workspace.ts +++ b/server/src/project/workspace.ts @@ -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); @@ -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()); } @@ -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); } /** From 3aeb069c3dc4af5d07c46f7cd9e047f5759307d2 Mon Sep 17 00:00:00 2001 From: sslinky <39886505+SSlinky@users.noreply.github.com> Date: Fri, 13 Jun 2025 19:32:13 +0800 Subject: [PATCH 3/4] Fixes #91 --- server/src/capabilities/capabilities.ts | 35 ++++++++----------------- 1 file changed, 11 insertions(+), 24 deletions(-) diff --git a/server/src/capabilities/capabilities.ts b/server/src/capabilities/capabilities.ts index 348f009..b02d0b0 100644 --- a/server/src/capabilities/capabilities.ts +++ b/server/src/capabilities/capabilities.ts @@ -287,7 +287,6 @@ export class ScopeItemCapability { 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. @@ -593,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; } From a81e1f3b8bdbd098f641c1701b0aa8bae4808cea Mon Sep 17 00:00:00 2001 From: sslinky <39886505+SSlinky@users.noreply.github.com> Date: Fri, 13 Jun 2025 19:32:46 +0800 Subject: [PATCH 4/4] 1.7.4 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 87c168f..693e3ba 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "vba-lsp", - "version": "1.7.3", + "version": "1.7.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "vba-lsp", - "version": "1.7.3", + "version": "1.7.4", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 495ed45..8c446ca 100644 --- a/package.json +++ b/package.json @@ -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"