Skip to content

Commit 2ba4128

Browse files
committed
Improve workspace-wide rename and refactor
1 parent b6a95e4 commit 2ba4128

File tree

2 files changed

+114
-45
lines changed

2 files changed

+114
-45
lines changed

server/src/analyser.ts

Lines changed: 88 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -427,6 +427,67 @@ export default class Analyzer {
427427
}
428428
}
429429

430+
/**
431+
* Like findOriginalDeclaration, but only searches within the file and doesn't
432+
* take parent scopes into consideration.
433+
*
434+
* TODO: Improve handling of multiple variable assignments and declarations in
435+
* one declaration command.
436+
*/
437+
public findOriginalDeclarationWithinFile({
438+
uri,
439+
word,
440+
kind,
441+
}: {
442+
uri: string
443+
word: string
444+
kind: LSP.SymbolKind
445+
}): LSP.Location | null {
446+
const rootNode = this.uriToAnalyzedDocument[uri]?.tree.rootNode
447+
448+
if (!rootNode) {
449+
return null
450+
}
451+
452+
let declaration: Parser.SyntaxNode | null | undefined
453+
let continueSearching = false
454+
455+
TreeSitterUtil.forEach(rootNode, (n) => {
456+
if ((declaration && !continueSearching) || n.type === 'subshell') {
457+
return false
458+
}
459+
460+
let definedSymbol: Parser.SyntaxNode | null | undefined
461+
462+
if (
463+
kind === LSP.SymbolKind.Variable &&
464+
(['declaration_command', 'variable_assignment', 'for_statement'].includes(
465+
n.type,
466+
) ||
467+
(n.type === 'command' && n.text.includes(':=')))
468+
) {
469+
definedSymbol = n.descendantsOfType('variable_name').at(0)
470+
} else if (kind === LSP.SymbolKind.Function && n.type === 'function_definition') {
471+
definedSymbol = n.firstNamedChild
472+
}
473+
474+
if (definedSymbol?.text === word) {
475+
declaration = definedSymbol
476+
continueSearching = n.type === 'command'
477+
}
478+
479+
if (n.type === 'function_definition') {
480+
return false
481+
}
482+
483+
return true
484+
})
485+
486+
return declaration
487+
? LSP.Location.create(uri, TreeSitterUtil.range(declaration))
488+
: null
489+
}
490+
430491
/**
431492
* Find all the locations where the given word was defined or referenced.
432493
* This will include commands, functions, variables, etc.
@@ -495,12 +556,14 @@ export default class Analyzer {
495556
word,
496557
kind,
497558
start,
559+
end,
498560
scope,
499561
}: {
500562
uri: string
501563
word: string
502564
kind: LSP.SymbolKind
503565
start?: LSP.Position
566+
end?: LSP.Position
504567
scope?: LSP.Range
505568
}): LSP.Range[] {
506569
const scopeNode = scope
@@ -526,6 +589,9 @@ export default class Analyzer {
526589
const startPosition = start
527590
? { row: start.line, column: start.character }
528591
: baseNode.startPosition
592+
const endPosition = end
593+
? { row: end.line + 1, column: end.character + 1 }
594+
: baseNode.endPosition
529595

530596
const ignoredRanges: LSP.Range[] = []
531597
const filter =
@@ -545,18 +611,26 @@ export default class Analyzer {
545611
const definedVariable = definition?.descendantsOfType('variable_name')?.at(0)
546612

547613
// Special handling for var="$var" cases
548-
if (definedVariable?.text === word && !n.equals(definedVariable)) {
549-
// `start?.line` is assumed to be the same as the variable's
550-
// original declaration line.
551-
if (definition?.startPosition.row === start?.line) {
552-
return false
614+
if (definedVariable?.text === word) {
615+
if (n.equals(definedVariable)) {
616+
// `end?.line` is assumed to be the same as the variable's
617+
// declaration line.
618+
if (definition?.startPosition.row === end?.line) {
619+
return false
620+
}
621+
} else {
622+
// `start?.line` is assumed to be the same as the variable's
623+
// original declaration line.
624+
if (definition?.startPosition.row === start?.line) {
625+
return false
626+
}
627+
628+
// Returning true here is a good enough heuristic for most
629+
// cases. It breaks down when redeclaration happens in multiple
630+
// nested scopes, handling those more complex situations can be
631+
// done later on if use cases arise.
632+
return true
553633
}
554-
555-
// Returning true here is a good enough heuristic for most cases.
556-
// It breaks down when redeclaration happens in multiple nested
557-
// scopes, handling those more complex situations can be done
558-
// later on if use cases arise.
559-
return true
560634
}
561635

562636
if (!parentScope || baseNode.equals(parentScope)) {
@@ -621,7 +695,7 @@ export default class Analyzer {
621695
}
622696

623697
return baseNode
624-
.descendantsOfType(typeOfDescendants, startPosition)
698+
.descendantsOfType(typeOfDescendants, startPosition, endPosition)
625699
.filter(filter)
626700
.map((n) => {
627701
if (n.type === 'function_definition' && n.firstNamedChild) {
@@ -867,9 +941,9 @@ export default class Analyzer {
867941
/**
868942
* If `includeAllWorkspaceSymbols` is true, this returns all URIs from the
869943
* background analysis. Else, it returns the URIs of the files that are
870-
* connected to `uri` via sourcing.
944+
* linked to `uri` via sourcing.
871945
*/
872-
public findAllConnectedUris(uri: string): string[] {
946+
public findAllLinkedUris(uri: string): string[] {
873947
if (this.includeAllWorkspaceSymbols) {
874948
return Object.keys(this.uriToAnalyzedDocument)
875949
}

server/src/server.ts

Lines changed: 26 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -766,53 +766,48 @@ export default class BashServer {
766766
kind: symbol.kind,
767767
})
768768

769-
const ranges = !declaration
770-
? this.analyzer.findOccurrencesWithin({
771-
uri: params.textDocument.uri,
772-
word: symbol.word,
773-
kind: symbol.kind,
774-
})
775-
: parent
776-
? this.analyzer.findOccurrencesWithin({
777-
uri: params.textDocument.uri,
778-
word: symbol.word,
779-
kind: symbol.kind,
780-
start: declaration.range.start,
781-
scope: parent.range,
782-
})
783-
: null
784-
if (ranges) {
769+
// File-wide rename
770+
if (!declaration || parent) {
785771
return <LSP.WorkspaceEdit>{
786772
changes: {
787-
[params.textDocument.uri]: ranges.map((r) =>
788-
LSP.TextEdit.replace(r, params.newName),
789-
),
773+
[params.textDocument.uri]: this.analyzer
774+
.findOccurrencesWithin({
775+
uri: params.textDocument.uri,
776+
word: symbol.word,
777+
kind: symbol.kind,
778+
start: declaration?.range.start,
779+
scope: parent?.range,
780+
})
781+
.map((r) => LSP.TextEdit.replace(r, params.newName)),
790782
},
791783
}
792784
}
793785

786+
// Workspace-wide rename
794787
const edits: LSP.WorkspaceEdit = {}
795-
if (declaration) {
796-
edits.changes = {}
797-
798-
edits.changes[declaration.uri] = this.analyzer
788+
edits.changes = {
789+
[declaration.uri]: this.analyzer
799790
.findOccurrencesWithin({
800791
uri: declaration.uri,
801792
word: symbol.word,
802793
kind: symbol.kind,
803794
start: declaration.range.start,
804795
})
805-
.map((r) => LSP.TextEdit.replace(r, params.newName))
806-
807-
for (const uri of this.analyzer.findAllConnectedUris(declaration.uri)) {
808-
edits.changes[uri] = this.analyzer
809-
.findOccurrencesWithin({
796+
.map((r) => LSP.TextEdit.replace(r, params.newName)),
797+
}
798+
for (const uri of this.analyzer.findAllLinkedUris(declaration.uri)) {
799+
edits.changes[uri] = this.analyzer
800+
.findOccurrencesWithin({
801+
uri,
802+
word: symbol.word,
803+
kind: symbol.kind,
804+
end: this.analyzer.findOriginalDeclarationWithinFile({
810805
uri,
811806
word: symbol.word,
812807
kind: symbol.kind,
813-
})
814-
.map((r) => LSP.TextEdit.replace(r, params.newName))
815-
}
808+
})?.range.start,
809+
})
810+
.map((r) => LSP.TextEdit.replace(r, params.newName))
816811
}
817812
return edits
818813
}

0 commit comments

Comments
 (0)