Skip to content

Commit 0232149

Browse files
committed
Implementing tree-sitter based indentation logic
- previously developed and tested in the sane-indentation package (> 0.9). Refer to atom/language-javascript#594 (comment) By itself this does nothing. The new logic is only used if the language package for the current language contains the necessary configuration (e.g., which scopes to indent on). So this PR goes together with, e.g., FILL-ME-IN in language-javascript.
1 parent 728aa0b commit 0232149

File tree

4 files changed

+151
-9
lines changed

4 files changed

+151
-9
lines changed

apm/package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/tree-indenter.js

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
2+
// const log = console.debug; // in dev
3+
const log = () => {}; // in production
4+
5+
6+
module.exports = class TreeIndenter {
7+
constructor (languageMode) {
8+
this.languageMode = languageMode;
9+
10+
this.scopes = atom.config.get('editor.scopes', {scope: this.languageMode.rootScopeDescriptor});
11+
log('constructor', this.scopes);
12+
}
13+
14+
/** tree indenter is configured for this language */
15+
get isConfigured() {
16+
return (!!this.scopes);
17+
}
18+
19+
// Given a position, walk up the syntax tree, to find the highest level
20+
// node that still starts here. This is to identify the column where this
21+
// node (e.g., an HTML closing tag) ends.
22+
_getHighestSyntaxNodeAtPosition(row, column = null) {
23+
if (column == null) {
24+
// Find the first character on the row that is not whitespace + 1
25+
column = this.languageMode.buffer.lineForRow(row).search(/\S/) + 1;
26+
}
27+
28+
let syntaxNode;
29+
if (column >= 0) {
30+
syntaxNode = this.languageMode.getSyntaxNodeAtPosition({row, column});
31+
while (syntaxNode && syntaxNode.parent
32+
&& syntaxNode.parent.startPosition.row == syntaxNode.startPosition.row
33+
&& syntaxNode.parent.endPosition.row == syntaxNode.startPosition.row
34+
&& syntaxNode.parent.startPosition.column == syntaxNode.startPosition.column
35+
) {
36+
syntaxNode = syntaxNode.parent;
37+
}
38+
return syntaxNode;
39+
}
40+
}
41+
42+
/** Walk up the tree. Everytime we meet a scope type, check whether we
43+
are coming from the first (resp. last) child. If so, we are opening
44+
(resp. closing) that scope, i.e., do not count it. Otherwise, add 1.
45+
46+
This is the core function.
47+
48+
It might make more sense to reverse the direction of this walk, i.e.,
49+
go from root to leaf instead.
50+
*/
51+
_treeWalk(node, lastScope = null) {
52+
if (node == null || node.parent == null) {
53+
return 0;
54+
} else {
55+
56+
let increment = 0;
57+
58+
const notFirstOrLastSibling =
59+
(node.previousSibling != null && node.nextSibling != null);
60+
61+
const isScope = this.scopes.indent[node.parent.type];
62+
(notFirstOrLastSibling && isScope && increment++);
63+
64+
const isScope2 = this.scopes.indentExceptFirst[node.parent.type];
65+
(!increment && isScope2 && node.previousSibling != null && increment++);
66+
67+
// check whether the last (lower) indentation happend due to a scope that
68+
// started on the same row and ends directly before this.
69+
// TODO: this currently only works for scopes that have a single-character
70+
// closing delimiter (like statement_blocks, but not HTML, for instance).
71+
if (lastScope && increment > 0
72+
&& // previous scope was a two-sided scope, reduce if starts on same row
73+
// and ends right before
74+
node.parent.startPosition.row == lastScope.node.startPosition.row
75+
&& node.parent.endIndex <= lastScope.node.endIndex + 1) {
76+
77+
log('ignoring repeat', node.parent.type, lastScope);
78+
increment = 0;
79+
}
80+
81+
// Adjusting based on node parent
82+
if (this.languageMode.grammar.precedingRowConditions
83+
&& node.parent.startPosition.row < node.startPosition.row
84+
&& this.languageMode.grammar.precedingRowConditions(node)) {
85+
log(`node adjustment -- previous row condition met`);
86+
increment += 1;
87+
}
88+
89+
log('treewalk', {node, notFirstOrLastSibling, type: node.parent.type, increment});
90+
const newLastScope = (isScope || isScope2 ? {node: node.parent} : lastScope);
91+
return this._treeWalk(node.parent, newLastScope) + increment;
92+
}
93+
};
94+
95+
96+
suggestedIndentForBufferRow(row, tabLength, options) {
97+
98+
this.precedingRowConditions = () => false;
99+
// const precedingRowConditions; // TODO
100+
101+
// get current indentation for current and preceding line
102+
const precedingRow = Math.max(row - 1, 0);
103+
const precedingLine = this.languageMode.buffer.lineForRow(precedingRow);
104+
const precedingIndentation = this.languageMode.indentLevelForLine(precedingLine, tabLength);
105+
const line = this.languageMode.buffer.lineForRow(row);
106+
const currentIndentation = this.languageMode.indentLevelForLine(line, tabLength);
107+
108+
const syntaxNode = this._getHighestSyntaxNodeAtPosition(row);
109+
if (!syntaxNode) {
110+
return 0;
111+
}
112+
let indentation = this._treeWalk(syntaxNode);
113+
114+
// apply current row, single line, type-based rules, e.g., 'else' or 'private:'
115+
this.scopes.types.indent[syntaxNode.type] && indentation++;
116+
this.scopes.types.outdent[syntaxNode.type] && indentation--;
117+
118+
// Special case for comments
119+
if (syntaxNode.type == 'comment'
120+
&& syntaxNode.startPosition.row < row
121+
&& syntaxNode.endPosition.row > row) {
122+
indentation += 1;
123+
}
124+
125+
if (options && options.preserveLeadingWhitespace) {
126+
indentation -= currentIndentation;
127+
}
128+
129+
return indentation;
130+
};
131+
132+
}

src/tree-sitter-language-mode.js

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ const Token = require('./token')
77
const TokenizedLine = require('./tokenized-line')
88
const TextMateLanguageMode = require('./text-mate-language-mode')
99
const {matcherForSelector} = require('./selectors')
10+
const TreeIndenter = require('./tree-indenter')
1011

1112
let nextId = 0
1213
const MAX_RANGE = new Range(Point.ZERO, Point.INFINITY).freeze()
@@ -188,13 +189,22 @@ class TreeSitterLanguageMode {
188189
}
189190

190191
suggestedIndentForBufferRow (row, tabLength, options) {
191-
return this._suggestedIndentForLineWithScopeAtBufferRow(
192-
row,
193-
this.buffer.lineForRow(row),
194-
this.rootScopeDescriptor,
195-
tabLength,
196-
options
197-
)
192+
if (!this.treeIndenter) {
193+
this.treeIndenter = new TreeIndenter(this)
194+
}
195+
196+
if (this.treeIndenter.isConfigured) {
197+
const indent = this.treeIndenter.suggestedIndentForBufferRow(row, tabLength, options)
198+
return indent
199+
} else {
200+
return this._suggestedIndentForLineWithScopeAtBufferRow(
201+
row,
202+
this.buffer.lineForRow(row),
203+
this.rootScopeDescriptor,
204+
tabLength,
205+
options
206+
)
207+
}
198208
}
199209

200210
indentLevelForLine (line, tabLength = tabLength) {

0 commit comments

Comments
 (0)