Skip to content

Commit 3f6acf7

Browse files
committed
Add input range tracking for AstNode
1 parent 27caf08 commit 3f6acf7

5 files changed

Lines changed: 87 additions & 18 deletions

File tree

libraries/rushell/src/AstNode.ts

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// See LICENSE in the project root for license information.
33

44
import { Token } from './Tokenizer';
5+
import { TextRange } from './TextRange';
56

67
export enum AstKind {
78
None,
@@ -18,27 +19,29 @@ export enum AstKind {
1819
*/
1920
export abstract class AstBaseNode {
2021
public readonly kind: AstKind = AstKind.None;
22+
public range: TextRange | undefined;
2123

2224
/**
2325
* Returns a diagnostic dump of the tree, showing the prefix/suffix/separator for
2426
* each node.
2527
*/
2628
public getDump(indent: string = ''): string {
27-
let result: string = indent + AstKind[this.kind];
29+
const nestedIndent: string = indent + ' ';
30+
let result: string = indent + '- ' + AstKind[this.kind] + ':\n';
2831

2932
const dumpText: string | undefined = this.getDumpText();
3033
if (dumpText) {
31-
result += '=' + JSON.stringify(dumpText);
34+
result += nestedIndent + 'Value=' + JSON.stringify(dumpText) + '\n';
35+
}
36+
37+
const fullRange: TextRange = this.getFullRange();
38+
if (!fullRange.isEmpty()) {
39+
result += nestedIndent + 'Range=' + JSON.stringify(fullRange.toString()) + '\n';
3240
}
3341

3442
const childNodes: AstBaseNode[] = this.getChildNodes();
35-
if (childNodes.length === 0) {
36-
result += '\n';
37-
} else {
38-
result += ':\n';
39-
for (const child of this.getChildNodes()) {
40-
result += child.getDump(indent + ' ');
41-
}
43+
for (const child of childNodes) {
44+
result += child.getDump(nestedIndent);
4245
}
4346

4447
return result;
@@ -50,6 +53,20 @@ export abstract class AstBaseNode {
5053
return nodes;
5154
}
5255

56+
public getFullRange(): TextRange {
57+
if (this.range) {
58+
return this.range;
59+
}
60+
61+
let encompassingRange: TextRange = TextRange.empty;
62+
63+
for (const child of this.getChildNodes()) {
64+
encompassingRange = encompassingRange.getEncompassingRange(child.getFullRange());
65+
}
66+
67+
return encompassingRange;
68+
}
69+
5370
protected abstract collectChildNodesInto(nodes: AstBaseNode[]): void;
5471

5572
protected getDumpText(): string | undefined {

libraries/rushell/src/Parser.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ export class Parser {
9393

9494
const astText: AstText = new AstText();
9595
astText.token = token;
96+
astText.range = token.range;
9697
return astText;
9798
}
9899

libraries/rushell/src/TextRange.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,45 @@ export class TextRange {
6565
return new TextRange(this.buffer, pos, end);
6666
}
6767

68+
public isEmpty(): boolean {
69+
return this.pos === this.end;
70+
}
71+
72+
/**
73+
* Returns the smallest TextRange object that encompasses both ranges. If there is a gap
74+
* between the two ranges, it will be included in the encompassing range.
75+
*/
76+
public getEncompassingRange(other: TextRange): TextRange {
77+
let newBuffer: string = this.buffer;
78+
79+
// Allow combining TextRange.empty with a TextRange from a different buffer
80+
if (other.buffer.length > 0) {
81+
newBuffer = other.buffer;
82+
83+
if (this.buffer.length > 0) {
84+
if (this.buffer !== other.buffer) {
85+
throw new Error('The ranges cannot be combined because they come from different buffers');
86+
}
87+
}
88+
}
89+
90+
let newPos: number = this.pos;
91+
let newEnd: number = this.end;
92+
93+
if (!other.isEmpty()) {
94+
if (this.isEmpty()) {
95+
newPos = other.pos;
96+
newEnd = other.end;
97+
} else {
98+
// Neither range is empty, so combine them
99+
newPos = Math.min(other.pos, this.pos);
100+
newEnd = Math.max(other.end, this.end);
101+
}
102+
}
103+
104+
return new TextRange(newBuffer, newPos, newEnd);
105+
}
106+
68107
/**
69108
* Returns the range from the associated string buffer.
70109
*/

libraries/rushell/src/test/Parser.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ function matchSnapshot(input: string): void {
1818
const result: AstScript = parser.parse();
1919
expect({
2020
input: escape(tokenizer.input.toString()),
21-
tree: result.getDump()
21+
tree: '\n' + result.getDump()
2222
}).toMatchSnapshot();
2323
}
2424

libraries/rushell/src/test/__snapshots__/Parser.test.ts.snap

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,26 @@
33
exports[`00: basic inputs 1`] = `
44
Object {
55
"input": "command arg1 arg2",
6-
"tree": "Script:
7-
Command:
8-
CompoundWord:
9-
Text=\\"command\\"
10-
CompoundWord:
11-
Text=\\"arg1\\"
12-
CompoundWord:
13-
Text=\\"arg2\\"
6+
"tree": "
7+
- Script:
8+
Range=\\"command arg1 arg2\\"
9+
- Command:
10+
Range=\\"command arg1 arg2\\"
11+
- CompoundWord:
12+
Range=\\"command\\"
13+
- Text:
14+
Value=\\"command\\"
15+
Range=\\"command\\"
16+
- CompoundWord:
17+
Range=\\"arg1\\"
18+
- Text:
19+
Value=\\"arg1\\"
20+
Range=\\"arg1\\"
21+
- CompoundWord:
22+
Range=\\"arg2\\"
23+
- Text:
24+
Value=\\"arg2\\"
25+
Range=\\"arg2\\"
1426
",
1527
}
1628
`;

0 commit comments

Comments
 (0)