Skip to content

Commit c1b8fc2

Browse files
committed
Improve wordAtPoint
Most of the implementation was done by @thomasjm in #119
1 parent a46509c commit c1b8fc2

File tree

2 files changed

+67
-2
lines changed

2 files changed

+67
-2
lines changed

server/src/analyser.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -319,13 +319,25 @@ export default class Analyzer {
319319
const document = this.uriToTreeSitterTrees[uri]
320320
const contents = this.uriToFileContent[uri]
321321

322-
const node = document.rootNode.namedDescendantForPosition({ row: line, column })
322+
if (!document.rootNode) {
323+
// Check for lacking rootNode (due to failed parse?)
324+
return null
325+
}
326+
327+
const point = { row: line, column }
328+
329+
const node = TreeSitterUtil.namedLeafDescendantForPosition(point, document.rootNode)
330+
331+
if (!node) {
332+
return null
333+
}
323334

324335
const start = node.startIndex
325336
const end = node.endIndex
326337
const name = contents.slice(start, end)
327338

328339
// Hack. Might be a problem with the grammar.
340+
// TODO: Document this with a test case
329341
if (name.endsWith('=')) {
330342
return name.slice(0, name.length - 1)
331343
}

server/src/util/tree-sitter.ts

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Range } from 'vscode-languageserver/lib/main'
2-
import { SyntaxNode } from 'web-tree-sitter'
2+
import { Point, SyntaxNode } from 'web-tree-sitter'
33

44
export function forEach(node: SyntaxNode, cb: (n: SyntaxNode) => void) {
55
cb(node)
@@ -52,3 +52,56 @@ export function findParent(
5252
}
5353
return null
5454
}
55+
56+
/**
57+
* Given a tree and a point, try to find the named leaf node that the point corresponds to.
58+
* This is a helper for wordAtPoint, useful in cases where the point occurs at the boundary of
59+
* a word so the normal behavior of "namedDescendantForPosition" does not find the desired leaf.
60+
* For example, if you do
61+
* > (new Parser()).setLanguage(bash).parse("echo 42").rootNode.descendantForIndex(4).text
62+
* then you get 'echo 42', not the leaf node for 'echo'.
63+
*
64+
* TODO: the need for this function might reveal a flaw in tree-sitter-bash.
65+
*/
66+
export function namedLeafDescendantForPosition(
67+
point: Point,
68+
rootNode: SyntaxNode,
69+
): SyntaxNode | null {
70+
const node = rootNode.namedDescendantForPosition(point)
71+
72+
if (node.childCount === 0) {
73+
return node
74+
} else {
75+
// The node wasn't a leaf. Try to figure out what word we should use.
76+
const nodeToUse = searchForLeafNode(point, node)
77+
if (nodeToUse) {
78+
return nodeToUse
79+
} else {
80+
return null
81+
}
82+
}
83+
}
84+
85+
function searchForLeafNode(point: Point, parent: SyntaxNode): SyntaxNode | null {
86+
let child: SyntaxNode = parent.firstNamedChild
87+
while (child) {
88+
if (
89+
pointsEqual(child.startPosition, point) ||
90+
pointsEqual(child.endPosition, point)
91+
) {
92+
if (child.childCount === 0) {
93+
return child
94+
} else {
95+
return searchForLeafNode(point, child)
96+
}
97+
}
98+
99+
child = child.nextNamedSibling
100+
}
101+
102+
return null
103+
}
104+
105+
function pointsEqual(point1: Point, point2: Point) {
106+
return point1.row === point2.row && point1.column === point2.column
107+
}

0 commit comments

Comments
 (0)