From c6ea1399f9fe1bb8988a6ddd06deea1983282534 Mon Sep 17 00:00:00 2001 From: Christian Fritz Date: Sun, 21 Oct 2018 21:13:37 -0700 Subject: [PATCH] Implementing tree-sitter based indentation logic - previously developed and tested in the sane-indentation package (> 0.9). Refer to https://github.com/atom/language-javascript/issues/594#issuecomment-420415412 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. --- apm/package-lock.json | 2 +- package-lock.json | 2 +- src/tree-indenter.js | 132 +++++++++++++++++++++++++++++++ src/tree-sitter-language-mode.js | 24 ++++-- 4 files changed, 151 insertions(+), 9 deletions(-) create mode 100644 src/tree-indenter.js diff --git a/apm/package-lock.json b/apm/package-lock.json index fcbb89967f18..614b07fa1081 100644 --- a/apm/package-lock.json +++ b/apm/package-lock.json @@ -4500,7 +4500,7 @@ }, "xmlbuilder": { "version": "0.4.3", - "resolved": "http://registry.npmjs.org/xmlbuilder/-/xmlbuilder-0.4.3.tgz", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-0.4.3.tgz", "integrity": "sha1-xGFLp04K0ZbmCcknLNnh3bKKilg=" }, "xmldom": { diff --git a/package-lock.json b/package-lock.json index abcf7f28ced1..914617326397 100644 --- a/package-lock.json +++ b/package-lock.json @@ -95,7 +95,7 @@ "bundled": true }, "semver": { - "version": "5.5.1", + "version": "5.6.0", "bundled": true } } diff --git a/src/tree-indenter.js b/src/tree-indenter.js new file mode 100644 index 000000000000..2915ef8f13b6 --- /dev/null +++ b/src/tree-indenter.js @@ -0,0 +1,132 @@ + +// const log = console.debug; // in dev +const log = () => {}; // in production + + +module.exports = class TreeIndenter { + constructor (languageMode) { + this.languageMode = languageMode; + + this.scopes = atom.config.get('editor.scopes', {scope: this.languageMode.rootScopeDescriptor}); + log('constructor', this.scopes); + } + + /** tree indenter is configured for this language */ + get isConfigured() { + return (!!this.scopes); + } + + // Given a position, walk up the syntax tree, to find the highest level + // node that still starts here. This is to identify the column where this + // node (e.g., an HTML closing tag) ends. + _getHighestSyntaxNodeAtPosition(row, column = null) { + if (column == null) { + // Find the first character on the row that is not whitespace + 1 + column = this.languageMode.buffer.lineForRow(row).search(/\S/) + 1; + } + + let syntaxNode; + if (column >= 0) { + syntaxNode = this.languageMode.getSyntaxNodeAtPosition({row, column}); + while (syntaxNode && syntaxNode.parent + && syntaxNode.parent.startPosition.row == syntaxNode.startPosition.row + && syntaxNode.parent.endPosition.row == syntaxNode.startPosition.row + && syntaxNode.parent.startPosition.column == syntaxNode.startPosition.column + ) { + syntaxNode = syntaxNode.parent; + } + return syntaxNode; + } + } + + /** Walk up the tree. Everytime we meet a scope type, check whether we + are coming from the first (resp. last) child. If so, we are opening + (resp. closing) that scope, i.e., do not count it. Otherwise, add 1. + + This is the core function. + + It might make more sense to reverse the direction of this walk, i.e., + go from root to leaf instead. + */ + _treeWalk(node, lastScope = null) { + if (node == null || node.parent == null) { + return 0; + } else { + + let increment = 0; + + const notFirstOrLastSibling = + (node.previousSibling != null && node.nextSibling != null); + + const isScope = this.scopes.indent[node.parent.type]; + (notFirstOrLastSibling && isScope && increment++); + + const isScope2 = this.scopes.indentExceptFirst[node.parent.type]; + (!increment && isScope2 && node.previousSibling != null && increment++); + + // check whether the last (lower) indentation happend due to a scope that + // started on the same row and ends directly before this. + // TODO: this currently only works for scopes that have a single-character + // closing delimiter (like statement_blocks, but not HTML, for instance). + if (lastScope && increment > 0 + && // previous scope was a two-sided scope, reduce if starts on same row + // and ends right before + node.parent.startPosition.row == lastScope.node.startPosition.row + && node.parent.endIndex <= lastScope.node.endIndex + 1) { + + log('ignoring repeat', node.parent.type, lastScope); + increment = 0; + } + + // Adjusting based on node parent + if (this.languageMode.grammar.precedingRowConditions + && node.parent.startPosition.row < node.startPosition.row + && this.languageMode.grammar.precedingRowConditions(node)) { + log(`node adjustment -- previous row condition met`); + increment += 1; + } + + log('treewalk', {node, notFirstOrLastSibling, type: node.parent.type, increment}); + const newLastScope = (isScope || isScope2 ? {node: node.parent} : lastScope); + return this._treeWalk(node.parent, newLastScope) + increment; + } + }; + + + suggestedIndentForBufferRow(row, tabLength, options) { + + this.precedingRowConditions = () => false; +// const precedingRowConditions; // TODO + + // get current indentation for current and preceding line + const precedingRow = Math.max(row - 1, 0); + const precedingLine = this.languageMode.buffer.lineForRow(precedingRow); + const precedingIndentation = this.languageMode.indentLevelForLine(precedingLine, tabLength); + const line = this.languageMode.buffer.lineForRow(row); + const currentIndentation = this.languageMode.indentLevelForLine(line, tabLength); + + const syntaxNode = this._getHighestSyntaxNodeAtPosition(row); + if (!syntaxNode) { + return 0; + } + let indentation = this._treeWalk(syntaxNode); + + // apply current row, single line, type-based rules, e.g., 'else' or 'private:' + this.scopes.types.indent[syntaxNode.type] && indentation++; + this.scopes.types.outdent[syntaxNode.type] && indentation--; + + // Special case for comments + if (syntaxNode.type == 'comment' + && syntaxNode.startPosition.row < row + && syntaxNode.endPosition.row > row) { + indentation += 1; + } + + if (options && options.preserveLeadingWhitespace) { + indentation -= currentIndentation; + } + + return indentation; + }; + +} diff --git a/src/tree-sitter-language-mode.js b/src/tree-sitter-language-mode.js index 3b40f489f9a7..2c38a3b6fa6c 100644 --- a/src/tree-sitter-language-mode.js +++ b/src/tree-sitter-language-mode.js @@ -7,6 +7,7 @@ const Token = require('./token') const TokenizedLine = require('./tokenized-line') const TextMateLanguageMode = require('./text-mate-language-mode') const {matcherForSelector} = require('./selectors') +const TreeIndenter = require('./tree-indenter') let nextId = 0 const MAX_RANGE = new Range(Point.ZERO, Point.INFINITY).freeze() @@ -188,13 +189,22 @@ class TreeSitterLanguageMode { } suggestedIndentForBufferRow (row, tabLength, options) { - return this._suggestedIndentForLineWithScopeAtBufferRow( - row, - this.buffer.lineForRow(row), - this.rootScopeDescriptor, - tabLength, - options - ) + if (!this.treeIndenter) { + this.treeIndenter = new TreeIndenter(this) + } + + if (this.treeIndenter.isConfigured) { + const indent = this.treeIndenter.suggestedIndentForBufferRow(row, tabLength, options) + return indent + } else { + return this._suggestedIndentForLineWithScopeAtBufferRow( + row, + this.buffer.lineForRow(row), + this.rootScopeDescriptor, + tabLength, + options + ) + } } indentLevelForLine (line, tabLength = tabLength) {