Skip to content

Commit 6236665

Browse files
authored
Merge pull request #95 from SSlinky/dev
Variable and Method Attribute Renaming
2 parents e793424 + 60dc789 commit 6236665

File tree

3 files changed

+147
-16
lines changed

3 files changed

+147
-16
lines changed

server/src/capabilities/capabilities.ts

Lines changed: 116 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -185,8 +185,10 @@ export enum ScopeType {
185185
VARIABLE,
186186
/** A variable declaration in a signature */
187187
PARAMETER,
188-
/** Any reference type that isn't a declaration. */
189-
REFERENCE
188+
/** A reference to some type of declaration. */
189+
REFERENCE,
190+
/** A special reference type. */
191+
ATTRIBUTE
190192
}
191193

192194
export enum AssignmentType {
@@ -211,6 +213,7 @@ export class ScopeItemCapability {
211213
};
212214
parameters?: Map<string, ScopeItemCapability[]>;
213215
references?: Map<string, ScopeItemCapability[]>;
216+
attributes?: Map<string, ScopeItemCapability[]>;
214217

215218
// Special scope references for easier resolution of names.
216219
implicitDeclarations?: Map<string, ScopeItemCapability[]>;
@@ -288,8 +291,10 @@ export class ScopeItemCapability {
288291
* Recursively build from this node down.
289292
*/
290293
build(): void {
291-
if (this.type === ScopeType.REFERENCE) {
292-
// Link to declaration if it exists.
294+
if (this.type === ScopeType.ATTRIBUTE) {
295+
this.resolveAttribute();
296+
this.validateAttributes();
297+
} else if (this.type === ScopeType.REFERENCE) {
293298
this.resolveLinks();
294299
this.validateLink();
295300
} else {
@@ -308,6 +313,7 @@ export class ScopeItemCapability {
308313
this.properties?.letters?.forEach(items => items.forEach(item => item.build()));
309314
this.properties?.setters?.forEach(items => items.forEach(item => item.build()));
310315
this.references?.forEach(items => items.forEach(item => item.build()));
316+
this.attributes?.forEach(items => items.forEach(item => item.build()));
311317

312318
this.isDirty = false;
313319
}
@@ -460,6 +466,88 @@ export class ScopeItemCapability {
460466
}
461467
}
462468

469+
private resolveAttribute() {
470+
/**
471+
* Most attributes will be belong to the parent. Variable attributes
472+
* will belong to the same scope as the item they refer to.
473+
*
474+
* We set one way links here to facilitate renaming.
475+
* Setting an attribute as a back link will impact unused diagnostics.
476+
*/
477+
478+
if (!this.parent) {
479+
Services.logger.error(`Expected parent for attribute ${this.name}`);
480+
throw new Error("Attribute scope item has no parent.");
481+
}
482+
483+
// The immediate parent is probably the linked item.
484+
if (this.name === this.parent.name) {
485+
this.link = this.parent;
486+
return;
487+
}
488+
489+
// If not, we may be a variable attribute (shared parent).
490+
const declarations = this.parent.properties?.getters?.get(this.name);
491+
if (!declarations) {
492+
return;
493+
}
494+
495+
// Handle a single declaration found.
496+
if (declarations.length === 1) {
497+
this.link = declarations[0];
498+
this.parent.moveAttribute(this, declarations[0]);
499+
return;
500+
}
501+
502+
// Handle duplicate declarations by attaching to the closest above.
503+
const targetRow = this.range?.start.line ?? 0;
504+
let closestDeclaration: ScopeItemCapability | undefined;
505+
for (const declaration of declarations) {
506+
const declarationRow = declaration?.range?.start.line ?? 0;
507+
if (declarationRow === 0 || declarationRow >= targetRow) {
508+
return;
509+
}
510+
511+
if (!closestDeclaration) {
512+
closestDeclaration = declaration;
513+
return;
514+
}
515+
516+
const closestRow = closestDeclaration.range?.start.line ?? 0;
517+
if (targetRow > declarationRow && declarationRow > closestRow) {
518+
closestDeclaration = declaration;
519+
}
520+
}
521+
522+
if (closestDeclaration) {
523+
this.link = closestDeclaration;
524+
this.parent.moveAttribute(this, closestDeclaration);
525+
}
526+
}
527+
528+
moveAttribute(attr: ScopeItemCapability, destination: ScopeItemCapability) {
529+
const items = this.attributes?.get(attr.name);
530+
if (!items || items.length === 0) {
531+
return;
532+
}
533+
534+
const unmoved: ScopeItemCapability[] = [];
535+
items.forEach(item => {
536+
const isLocMatch = item.locationUri === attr.locationUri;
537+
const isRangeMatch = rangeEquals(item.element?.context.range, attr.range);
538+
if (isLocMatch && isRangeMatch) {
539+
destination.attributes ??= new Map();
540+
destination.addItem(destination.attributes, item);
541+
} else {
542+
unmoved.push(item);
543+
}
544+
});
545+
}
546+
547+
private validateAttributes() {
548+
// Attributes must be in specific locations to work.
549+
}
550+
463551
private resolveLinks() {
464552

465553
// Resolve where we have no member access names.
@@ -726,6 +814,13 @@ export class ScopeItemCapability {
726814
return this;
727815
}
728816

817+
// Register attributes
818+
if (item.type === ScopeType.ATTRIBUTE) {
819+
item.parent.attributes ??= new Map();
820+
item.parent.addItem(item.parent.attributes, item);
821+
return this;
822+
}
823+
729824
// Add implicitly accessible names to the project scope.
730825
if (item.isPublicScope && this.project && this !== this.project) {
731826
this.project.implicitDeclarations ??= new Map();
@@ -913,10 +1008,16 @@ export class ScopeItemCapability {
9131008

9141009
private getItemsIdentifiedAtPosition(position: Position, results: ScopeItemCapability[] = [], searchItems: ScopeItemCapability[] = []): void {
9151010
while (searchItems.length > 0) {
1011+
// Get the next scope to search.
9161012
const scope = searchItems.pop();
1013+
if (scope === undefined) continue;
1014+
1015+
// Get the standard maps and add attributes to them if they exist.
1016+
const scopeMaps = scope.maps ?? [];
1017+
if (scope.attributes) scopeMaps.push(scope.attributes);
9171018

9181019
// Check all items for whether they have a name overlap or a scope overlap.
919-
scope?.maps.forEach(map => map.forEach(items => items.forEach(item => {
1020+
scopeMaps.forEach(map => map.forEach(items => items.forEach(item => {
9201021
const elementRange = item.range;
9211022
const identifierRange = item.element?.identifierCapability?.range;
9221023
if (identifierRange && isPositionInsideRange(position, identifierRange)) {
@@ -931,13 +1032,13 @@ export class ScopeItemCapability {
9311032
}
9321033

9331034
getRenameItems(uri: string, position: Position): ScopeItemCapability[] {
934-
const module = this.findModuleByUri(uri);
935-
if (!module) {
1035+
const moduleScope = this.findModuleByUri(uri);
1036+
if (!moduleScope) {
9361037
return [];
9371038
}
9381039

9391040
const itemsAtPosition: ScopeItemCapability[] = [];
940-
this.getItemsIdentifiedAtPosition(position, itemsAtPosition, [module]);
1041+
this.getItemsIdentifiedAtPosition(position, itemsAtPosition, [moduleScope]);
9411042
if (itemsAtPosition.length === 0) {
9421043
Services.logger.warn(`Nothing to rename.`);
9431044
return [];
@@ -956,14 +1057,15 @@ export class ScopeItemCapability {
9561057
item.parent.properties.letters?.get(item.identifier)
9571058
]
9581059
: item
959-
).flat().flat().flat().filter(x => !!x);
1060+
).flat(2).filter(x => !!x);
9601061

961-
// Add backlinks for each item.
962-
const addedBacklinks = propertyIncludedItems.map(item =>
963-
item.backlinks ? [item, ...item.backlinks] : item
964-
).flat().flat();
1062+
// Add backlinks and attributes for each item.
1063+
const addedReferences = propertyIncludedItems.map(item => [
1064+
item.backlinks ? [item, ...item.backlinks] : item,
1065+
item.attributes?.get(item.name) ? [item, ...item.attributes.get(item.name)!] : item
1066+
]).flat(2);
9651067

966-
const uniqueItemsAtPosition = this.removeDuplicatesByRange(addedBacklinks);
1068+
const uniqueItemsAtPosition = this.removeDuplicatesByRange(addedReferences);
9671069
return uniqueItemsAtPosition;
9681070
}
9691071

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// Core
2+
import { TextDocument } from "vscode-languageserver-textdocument";
3+
4+
// Antlr
5+
import { AttributeStatementContext } from "../../antlr/out/vbaParser";
6+
7+
// Project
8+
import { BaseRuleSyntaxElement } from "./base";
9+
import { IdentifierCapability, ScopeItemCapability, ScopeType } from "../../capabilities/capabilities";
10+
11+
12+
export class AttributeElement extends BaseRuleSyntaxElement<AttributeStatementContext> {
13+
identifierCapability: IdentifierCapability;
14+
scopeItemCapability: ScopeItemCapability;
15+
16+
constructor(ctx: AttributeStatementContext, doc: TextDocument) {
17+
super(ctx, doc);
18+
19+
this.identifierCapability = new IdentifierCapability(this, () => ctx.ambiguousIdentifier());
20+
this.scopeItemCapability = new ScopeItemCapability(this, ScopeType.ATTRIBUTE);
21+
}
22+
}

server/src/project/parser/vbaListener.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
AmbiguousIdentifierContext,
1212
AnyOperatorContext,
1313
ArgumentListContext,
14+
AttributeStatementContext,
1415
CallStatementContext,
1516
ClassModuleContext,
1617
DictionaryAccessExpressionContext,
@@ -44,7 +45,7 @@ import {
4445
WithStatementContext
4546
} from '../../antlr/out/vbaParser';
4647
import {
47-
AttributeStatementContext,
48+
AttributeStatementContext as FmtAttributeStatementContext,
4849
BasicStatementContext,
4950
BlockContext,
5051
CaseDefaultStatementContext,
@@ -88,6 +89,7 @@ import {
8889
PropertyLetDeclarationElement,
8990
PropertySetDeclarationElement,
9091
} from '../elements/procedure';
92+
import { AttributeElement } from '../elements/attributes';
9193

9294

9395
enum ParserAssignmentState {
@@ -181,6 +183,11 @@ export class VbaListener extends vbaListener {
181183
this.document.registerElement(element);
182184
};
183185

186+
enterAttributeStatement = (ctx: AttributeStatementContext) => {
187+
const element = new AttributeElement(ctx, this.document.textDocument);
188+
this.document.registerElement(element);
189+
};
190+
184191
enterEnumDeclaration = (ctx: EnumDeclarationContext) => {
185192
const element = new EnumDeclarationElement(ctx, this.document.textDocument, this.isAfterMethodDeclaration);
186193
this.document.registerElement(element);
@@ -613,7 +620,7 @@ export class VbaFmtListener extends vbafmtListener {
613620
}
614621

615622
// Attributes are always zero indented.
616-
enterAttributeStatement = (ctx: AttributeStatementContext) => {
623+
enterAttributeStatement = (ctx: FmtAttributeStatementContext) => {
617624
const range = this.getCtxRange(ctx);
618625
const offset = ctx.endsWithLineEnding ? 0 : 1;
619626

0 commit comments

Comments
 (0)