I am in hell

This commit is contained in:
Andrew Dupont 2023-03-03 11:02:32 -08:00
parent e1cce9ac9a
commit 917fdf551b
6 changed files with 893 additions and 210 deletions

View File

@ -7,7 +7,7 @@ module.exports = {
extends: [
"eslint:recommended",
"plugin:node/recommended",
"plugin:jsdoc/recommended"
// "plugin:jsdoc/recommended"
],
overrides: [],
parserOptions: {

View File

@ -1,5 +1,5 @@
name: 'Ruby'
scopeName: 'source.ruby'
name: 'Ruby (TS)'
scopeName: 'source.ruby.ts'
type: 'tree-sitter'
parser: 'tree-sitter-ruby'

View File

@ -1,5 +1,342 @@
; Keywords
; NOTES:
;
; (#set! final "true") means that any later rule that matches this exact range
; will be ignored.
;
; (#set! shy "true") means that this rule will be ignored if any previous rule
; has marked this exact range, whether or not it was marked as final.
(superclass
(constant) @entity.name.type.class.ruby
.
)
(superclass
"<" @punctuation.separator.inheritance.ruby
(constant) @entity.other.inherited-class.ruby
(#set! final "true")
)
; module [Foo]
(module
name: (constant) @entity.name.type.module.ruby
(#set! final "true")
)
(singleton_class
"<<" @keyword.operator.assigment.ruby
)
(call
method: (identifier) @keyword.other.special-method (#match? @keyword.other.special-method "^(raise|loop)$")
)
; Mark `new` as a special method in all contexts, from `Foo.new` to
; `Foo::Bar::Baz.new` and so on.
(call
receiver: (_)
method: (identifier) @function.method.builtin.ruby
(#eq? @function.method.builtin.ruby "new")
)
(superclass
(scope_resolution
scope: (constant) @entity.other.inherited-class.ruby
name: (constant) @entity.other.inherited-class.ruby
)
)
(scope_resolution
scope: (constant) @support.class.ruby
(#set! final "true")
)
(scope_resolution
"::" @keyword.operator.namespace.ruby
(#set! final "true")
)
(scope_resolution
name: (constant) @support.class.ruby
(#set! final "true")
)
; ((variable) @keyword.other.special-method
; (#match? @keyword.other.special-method "^(extend)$"))
((identifier) @keyword.other.special-method
(#match? @keyword.other.special-method "^(private|protected|public)$"))
; Highlight the interpolation inside of a string, plus the strings that delimit
; the interpolation.
(
(interpolation
"#{" @punctuation.section.embedded.begin.ruby
"}" @punctuation.section.embedded.end.ruby
) @meta.embedded
(#set! final "true")
)
; Function calls
; TODO: The TM grammar scopes this as `keyword.control.pseudo-method.ruby`; decide on
; the best name for it.
(
(identifier) @function.method.builtin.ruby
(#eq? @function.method.builtin.ruby "require")
)
(unary
"defined?" @function.method.builtin.ruby
)
(class name: [(constant)]
@entity.name.type.class.ruby
(#set! final "true"))
; Scope the entire inside of a class body to `meta.class.ruby`; it's
; semantically useful even though it probably won't get highlighted.
(class) @meta.class.ruby
(method
name: [(identifier) (constant)] @entity.name.function.ruby
(#set! final "true")
)
; (singleton_method name: [(identifier) (constant)] @function.method)
; Identifiers
(global_variable) @variable.other.readwrite.global.ruby
(class_variable) @variable.other.readwrite.class.ruby
(instance_variable) @variable.other.readwrite.instance.ruby
(exception_variable (identifier) @variable.parameter.ruby)
(call receiver: (identifier) @variable.other.ruby)
; (call
; receiver: (constant) @support.class.ruby
; method: (identifier) @function.method.builtin.ruby
; (#eq? @function.method.builtin.ruby "new")
; )
(call
method: [(identifier) (constant)] @keyword.other.special-method (#match? @keyword.other.special-method "^(extend)$"))
; (call
; method: [(identifier) (constant)] @function.method)
; (call
; method: (scope_resolution
; scope: [(constant) (scope_resolution)] @support.class.ruby
; "::" @keyword.operator.namespace.ruby
; name: [(constant)] @support.class.ruby
; )
; )
(scope_resolution
scope: [(constant) (scope_resolution)]
"::" @keyword.operator.namespace.ruby
name: [(constant)] @support.class.ruby
(#set! final "true")
)
; (call
; receiver: (constant) @constant.ruby (#match? @constant.ruby "^[A-Z\\d_]+$")
; )
(call receiver: (constant)
@support.class.ruby
(#set! final "true")
)
((identifier) @constant.builtin.ruby
(#match? @constant.builtin.ruby "^__(FILE|LINE|ENCODING)__$"))
; Anything that hasn't been highlighted yet is probably a bare identifier. Mark
; it as `constant` if it's all uppercase…
((constant) @constant.ruby
(#match? @constant.ruby "^[A-Z\\d_]+$")
(#set! final "true"))
; …otherwise treat it as a variable.
((constant) @variable.other.constant.ruby)
(self) @variable.language.self.ruby
(super) @keyword.control.pseudo-method.ruby
(block_parameter (identifier) @variable.parameter.function.block.ruby)
(block_parameters (identifier) @variable.parameter.function.block.ruby)
(destructured_parameter (identifier) @variable.parameter.function.ruby)
(hash_splat_parameter (identifier) @variable.parameter.function.splat.ruby)
(lambda_parameters (identifier) @variable.parameter.function.lambda.ruby)
(method_parameters (identifier) @variable.parameter.function.ruby)
(splat_parameter (identifier) @variable.parameter.function.splat.ruby)
; TODO: We might want to combine the name and the colon so they can get
; highlighted together as one scope. Pretty sure there's a way to do that.
; A keyword-style parameter when defining a method.
(keyword_parameter
":" @constant.other.symbol.parameter.ruby
(#set! final "true")
)
; A keyword-style argument when calling a method.
(pair
key: (hash_key_symbol) @constant.other.symbol.hashkey.ruby
":" @constant.other.symbol.hashkey.ruby
(#set! final "true")
)
(optional_parameter
name: (identifier) @variable.parameter.function.optional.ruby
)
(
(identifier) @support.function.kernel.ruby
(#match? @support.function.kernel.ruby "^(abort|at_exit|autoload|binding|callcc|caller|caller_locations|chomp|chop|eval|exec|exit|fork|format|gets|global_variables|gsub|lambda|load|local_variables|open|p|print|printf|proc|putc|puts|rand|readline|readlines|select|set_trace_func|sleep|spawn|sprintf|srand|sub|syscall|system|test|trace_var|trap|untrace_var|warn)$")
(#set! final "true")
)
;((constant) @constant.ruby
; (#match? @constant.ruby "^[A-Z\\d_]+$"))
; (identifier) @variable
; Literals
; TODO: I can't mark these as @string.quoted.double.ruby yet because the "s
; match _any_ delimiter, including single quotes and %Qs. This is probably a
; bug in tree-sitter-ruby.
(
(string
"\"" @punctuation.definition.string.begin.ruby
(string_content)
"\"" @punctuation.definition.string.end.ruby
) @string.quoted.ruby
(#set! final "true")
)
; (will match empty strings)
(string) @string.quoted.ruby
[
(bare_string)
(subshell)
(heredoc_body)
(heredoc_beginning)
] @string.unquoted.ruby
[
(simple_symbol)
(delimited_symbol)
(hash_key_symbol)
(bare_symbol)
] @constant.other.symbol.ruby
(regex) @string.special.regex
(escape_sequence) @escape
[
(integer)
(float)
] @constant.numeric.ruby
(nil) @constant.language.nil.ruby
[
(true)
(false)
] @constant.language.boolean.ruby
; TODO: tree-sitter-ruby doesn't currently let us distinguish line comments
; from block comments (the =begin/=end syntax). Until it does, the latter will
; be scoped as `comment.line` and we just have to live with it — or invent a
; way to hack around it in the language mode file once we can inspect the text
; itself.
;
; Likewise, we can't grab the leading `#` and scope it as punctuation the way
; the TM grammar does.
(comment) @comment.line.number-sign.ruby
; To distinguish them from the bitwise "|" operator.
(block_parameters
"|" @punctuation.separator.variable.ruby
)
(binary
"|" @keyword.operator.other.ruby
)
; Operators
"(" @punctuation.brace.round.begin.ruby
")" @punctuation.brace.round.end.ruby
"[" @punctuation.brace.square.begin.ruby
"]" @punctuation.brace.square.end.ruby
"{" @punctuation.brace.curly.begin.ruby
"}" @punctuation.brace.curly.end.ruby
(conditional
["?" ":"] @keyword.operator.conditional.ruby
(#set! final "true")
)
[
"="
"||="
"+="
"-="
"<<"
] @keyword.operator.assigment.ruby
[
"||"
"&&"
] @keyword.operator.logical.ruby
[
"&"
] @keyword.operator.other.ruby
[
"=="
">="
"<="
">"
"<"
] @keyword.operator.comparison.ruby
[
"+"
"-"
"*"
"/"
"**"
] @keyword.operator.arithmetic.ruby
[
"=>"
"->"
] @keyword.operator.ruby
[
","
";"
"."
":"
] @punctuation.separator.ruby
; Keywords
[
"alias"
"and"
@ -28,119 +365,10 @@
"when"
"while"
"yield"
] @keyword
] @keyword.control.ruby
((identifier) @keyword
(#match? @keyword "^(private|protected|public)$"))
; Function calls
((identifier) @function.method.builtin
(#eq? @function.method.builtin "require"))
"defined?" @function.method.builtin
(call
method: [(identifier) (constant)] @function.method)
; Function definitions
(alias (identifier) @function.method)
(setter (identifier) @function.method)
(method name: [(identifier) (constant)] @function.method)
(singleton_method name: [(identifier) (constant)] @function.method)
; Identifiers
[
(class_variable)
(instance_variable)
] @property
((identifier) @constant.builtin
(#match? @constant.builtin "^__(FILE|LINE|ENCODING)__$"))
((constant) @constant
(#match? @constant "^[A-Z\\d_]+$"))
(constant) @constructor
(self) @variable.builtin
(super) @variable.builtin
(block_parameter (identifier) @variable.parameter)
(block_parameters (identifier) @variable.parameter)
(destructured_parameter (identifier) @variable.parameter)
(hash_splat_parameter (identifier) @variable.parameter)
(lambda_parameters (identifier) @variable.parameter)
(method_parameters (identifier) @variable.parameter)
(splat_parameter (identifier) @variable.parameter)
(keyword_parameter name: (identifier) @variable.parameter)
(optional_parameter name: (identifier) @variable.parameter)
((identifier) @function.method
(#is-not? local))
(identifier) @variable
; Literals
[
(string)
(bare_string)
(subshell)
(heredoc_body)
(heredoc_beginning)
] @string
[
(simple_symbol)
(delimited_symbol)
(hash_key_symbol)
(bare_symbol)
] @string.special.symbol
(regex) @string.special.regex
(escape_sequence) @escape
[
(integer)
(float)
] @number
[
(nil)
(true)
(false)
]@constant.builtin
(interpolation
"#{" @punctuation.special
"}" @punctuation.special) @embedded
(comment) @comment
; Operators
[
"="
"=>"
"->"
] @operator
[
","
";"
"."
] @punctuation.delimiter
[
"("
")"
"["
"]"
"{"
"}"
"%w("
"%i("
] @punctuation.bracket
; Any identifiers we haven't caught yet can be given a generic scope.
((identifier) @function.method.ruby
(#is-not? local)
(#set! shy "true")
)

View File

@ -0,0 +1,21 @@
[
"do"
"module"
"class"
] @indent
; If the user has typed `if foo` but hasn't typed `end` yet, the only way to
; recognize that we should indent is via these anonymous nodes… TO
"if" @indent
"unless" @indent
; …but that also improperly catches postfix conditionals like `exit if foo`. So
; we dedent those to balance it out.
(if_modifier) @dedent
(unless_modifier) @dedent
[
"end"
] @dedent

View File

@ -1,24 +1,29 @@
[
(class)
"class"
(singleton_class)
(method)
"def"
(singleton_method)
(module)
(if)
(block)
(do_block)
(argument_list)
"module"
"if"
"else"
"unless"
; (block)
; (argument_list)
(case)
(while)
(until)
(for)
(begin)
(unless)
"begin"
"do"
"rescue"
; (unless)
"("
"{"
"["
] @indent
[
"end"
")"
@ -34,7 +39,7 @@
(when)
(elsif)
(else)
(rescue)
"rescue"
(ensure)
] @branch

View File

@ -1,11 +1,132 @@
const Parser = require('web-tree-sitter');
const ScopeDescriptor = require('./scope-descriptor')
const fs = require('fs');
// const fs = require('fs');
const { Point, Range } = require('text-buffer');
const { Emitter } = require('event-kit');
const initPromise = Parser.init()
createTree = require("./rb-tree")
const createTree = require("./rb-tree")
class PositionIndex {
constructor () {
this.map = new Map
// TODO: It probably doesn't actually matter what order these are visited
// in.
this.order = []
this.rangeData = new Map
}
_normalizePoint (point) {
return `${point.row},${point.column}`
}
_normalizeRange (syntax) {
let { startPosition, endPosition } = syntax.node;
return `${this._normalizePoint(startPosition)}/${this._normalizePoint(endPosition)}`
}
_keyToObject (key) {
let [row, column] = key.split(',');
return { row: Number(row), column: Number(column) }
}
setDataForRange (syntax, props) {
let key = this._normalizeRange(syntax);
return this.rangeData.set(key, props);
}
getDataForRange (syntax) {
let key = this._normalizeRange(syntax);
return this.rangeData.get(key);
}
store (syntax, id) {
let {
node,
setProperties: props = {}
} = syntax;
let {
startPosition: start,
endPosition: end
} = node;
let data = this.getDataForRange(syntax);
if (data && data.final) {
// A previous rule covering this exact range marked itself as "final." We
// should not add an additional scope.
return;
} else if (data && props.shy) {
// This node will only apply if we haven't yet marked this range with
// anything.
return;
} else {
// TODO: We may want to handle the case where more than one token will
// want to set data for a given range. Do we merge objects? Store each
// dataset separately?
this.setDataForRange(syntax, props);
}
// We should open this scope at `start`.
this.set(node, start, id, 'open');
// We should close this scope at `end`.
this.set(node, end, id, 'close');
}
set (node, point, id, which) {
let key = this._normalizePoint(point)
if (!this.order.includes(key)) {
this.order.push(key);
}
if (!this.map.has(key)) {
this.map.set(key, {
open: [],
close: [],
openNodes: [],
closeNodes: []
})
}
let bundle = this.map.get(key);
let idBundle = bundle[which];
let nodeBundle = bundle[`${which}Nodes`];
if (which === 'open') {
// TODO: For now, assume that if two tokens both open at (X, Y), the one
// that spans a greater distance in the buffer will be encountered first.
// If that's not true, this logic may need to be more complex.
// If an earlier token has already opened at this point, we want to open
// after it.
idBundle.push(id)
nodeBundle.push(node)
} else {
// If an earlier token has already closed at this point, we want to close
// before it.
idBundle.unshift(id)
nodeBundle.unshift(node)
}
}
get (point) {
let key = this._normalizePoint(point)
return this.map.get(key)
}
clear () {
this.map.clear()
this.rangeData.clear()
this.order = []
}
*[Symbol.iterator] () {
for (let key of this.order) {
let point = this._keyToObject(key);
yield [point, this.map.get(key)]
}
}
}
const VAR_ID = 257
class WASMTreeSitterLanguageMode {
@ -29,7 +150,7 @@ class WASMTreeSitterLanguageMode {
this.lang = lang
this.syntaxQuery = lang.query(grammar.syntaxQuery)
if(grammar.localsQuery) {
this.localsQuery = lang.query(grammar.localsQuery)
// this.localsQuery = lang.query(grammar.localsQuery)
}
this.grammar = grammar
if(grammar.foldsQuery) {
@ -41,7 +162,7 @@ class WASMTreeSitterLanguageMode {
// Force first highlight
this.boundaries = createTree(comparePoints)
const startRange = new Range([0, 0], [0, 0])
// const startRange = new Range([0, 0], [0, 0])
const range = buffer.getRange()
this.tree = this.parser.parse(buffer.getText())
this.emitter.emit('did-change-highlighting', range)
@ -53,6 +174,22 @@ class WASMTreeSitterLanguageMode {
});
}
// A hack to force an existing buffer to react to an update in the SCM file.
_reloadSyntaxQuery () {
// let _oldSyntaxQuery = this.syntaxQuery;
this.grammar._reloadQueryFiles()
let lang = this.parser.getLanguage()
this.syntaxQuery = lang.query(this.grammar.syntaxQuery)
// let range = this.buffer.getRange()
// this._updateSyntax(range.start, range.end)
// Force first highlight
this.boundaries = createTree(comparePoints)
// const startRange = new Range([0, 0], [0, 0])
const range = this.buffer.getRange()
this.tree = this.parser.parse(this.buffer.getText())
this.emitter.emit('did-change-highlighting', range)
}
getGrammar() {
return this.grammar
}
@ -101,59 +238,160 @@ class WASMTreeSitterLanguageMode {
}
}
_updateSyntax(from, to) {
const syntax = this.syntaxQuery.captures(this.tree.rootNode, from, to)
let oldDataIterator = this.boundaries.ge(from)
let oldScopes = []
_findInvalidationRange(from, to) {
let iterate = (from, to) => {
let iterator = this.boundaries.ge(from)
while( oldDataIterator.hasNext && comparePoints(oldDataIterator.key, to) <= 0 ) {
this.boundaries = this.boundaries.remove(oldDataIterator.key)
oldScopes = oldDataIterator.value.closeScopeIds
oldDataIterator.next()
let newFrom = from;
let newTo = to;
while (iterator.hasNext && comparePoints(iterator.key, to) <= 0) {
let { key, value } = iterator
let { closeNodes, openNodes } = value
for (let o of openNodes) {
if (comparePoints(o.endPosition, newTo) > 0) {
newTo = o.endPosition;
}
}
for (let c of closeNodes) {
if (comparePoints(c.startPosition, newFrom) < 0) {
newFrom = c.startPosition;
}
}
iterator.next()
}
let didChange = comparePoints(from, newFrom) !== 0 || comparePoints(to, newTo) !== 0;
return [newFrom, newTo, didChange];
};
let currentFrom = from;
let currentTo = to;
let stable = false;
while (!stable) {
let [newFrom, newTo, didChange] = iterate(currentFrom, currentTo);
if (!didChange) {
stable = true;
break;
}
currentFrom = newFrom;
currentTo = newTo;
}
return [currentFrom, currentTo];
}
_updateSyntax(from, to) {
console.log('_updateSyntax', from, to);
let [realFrom, realTo] = this._findInvalidationRange(from, to)
console.log('(widening to:)', realFrom, realTo);
from = realFrom;
to = realTo;
const syntax = this.syntaxQuery.captures(this.tree.rootNode, from, to)
console.log('captures:', syntax);
let oldDataIterator = this.boundaries.ge(from)
let oldScopes = []
// Remove all boundaries data for the given range.
while (oldDataIterator.hasNext && comparePoints(oldDataIterator.key, to) <= 0 ) {
let { key, value } = oldDataIterator
this.boundaries = this.boundaries.remove(key)
oldScopes = value.closeScopeIds
oldDataIterator.next()
// TODO: Doesn't this mean that we'll miss the last item in the iterator
// under certain circumstances?
}
// TODO: Still don't quite understand this; need to revisit.
oldScopes = oldScopes || []
syntax.forEach(({node, name}) => {
let id = this.scopeNames.get(name)
if(!id) {
this.lastId += 2
id = this.lastId
const newId = this.lastId;
this.scopeNames.set(name, newId)
this.scopeIds.set(newId, name)
}
// })
let old = this.boundaries.get(node.startPosition)
if(old) {
old.openNode = node
if(old.openScopeIds.length === 0) {
old.openScopeIds = [id]
}
} else {
this.boundaries = this.boundaries.insert(node.startPosition, {
closeScopeIds: [...oldScopes],
openScopeIds: [id],
openNode: node,
position: node.startPosition
})
oldScopes = [id]
}
if (!this.positionIndex) {
this.positionIndex = new PositionIndex();
}
this.positionIndex.clear()
old = this.boundaries.get(node.endPosition)
if(old) {
old.closeNode = node
if(old.closeScopeIds.length === 0) old.closeScopeIds = [id]
} else {
this.boundaries = this.boundaries.insert(node.endPosition, {
closeScopeIds: [id],
openScopeIds: [],
closeNode: node,
position: node.endPosition
})
}
})
console.log('oldScopes:', oldScopes.map(s => this.scopeForId(s)));
let inspect = (id) => {
if (Array.isArray(id)) {
return id.map(i => inspect(i))
}
return this.scopeForId(id)
};
syntax.forEach((s) => {
let { name } = s
let id = this.findOrCreateScopeId(name)
// PositionIndex takes all our syntax tokens and consolidates them into a
// fixed set of boundaries to visit in order. If a token has data, it
// sets that data so that a later token for the same range can read it.
this.positionIndex.store(s, id)
});
for (let [point, data] of this.positionIndex) {
let bundle = {
closeScopeIds: [...data.close],
openScopeIds: [...data.open],
closeNodes: [...data.closeNodes],
openNodes: [...data.openNodes],
position: point
}
console.log('inserting', bundle, 'at point:', point, 'close:', inspect(bundle.closeScopeIds), 'open:', inspect(bundle.openScopeIds));
this.boundaries = this.boundaries.insert(point, bundle)
}
// syntax.forEach(({ node, name }) => {
// // let id = this.scopeNames.get(name)
// // console.log(' handling node:', name, node);
// // if (!id) {
// // this.lastId += 2
// // id = this.lastId
// // const newId = this.lastId;
// // this.scopeNames.set(name, newId)
// // this.scopeIds.set(newId, name)
// // }
// let id = this.findOrCreateScopeId(name)
// let old = this.boundaries.get(node.startPosition)
// if (old) {
// // console.log(' found node:', this.scopeForId(id));
// old.openNode = node
// if (old.openScopeIds.length === 0) {
// old.openScopeIds = [id]
// }
// } else {
// let bundle = {
// closeScopeIds: [...oldScopes],
// openScopeIds: [id],
// openNode: node,
// position: node.startPosition
// }
// console.log('inserting close', s(bundle.closeScopeIds), 'open', s(bundle.openScopeIds), 'at', node.startPosition);
// this.boundaries = this.boundaries.insert(node.startPosition, bundle)
// oldScopes = [id]
// }
//
// old = this.boundaries.get(node.endPosition)
// if (old) {
// old.closeNode = node
// if (old.closeScopeIds.length === 0) {
// old.closeScopeIds = [id]
// }
// } else {
// this.boundaries = this.boundaries.insert(node.endPosition, {
// closeScopeIds: [id],
// openScopeIds: [],
// closeNode: node,
// position: node.endPosition
// })
// }
// })
console.log('closing', oldScopes.map(s => this.scopeForId(s)), 'at the end of the document');
this.boundaries = this.boundaries.insert(Point.INFINITY, {
closeScopeIds: [...oldScopes],
openScopeIds: [],
@ -161,6 +399,66 @@ class WASMTreeSitterLanguageMode {
})
}
// _updateSyntax(from, to) {
// const syntax = this.syntaxQuery.captures(this.tree.rootNode, from, to)
// let oldDataIterator = this.boundaries.ge(from)
// let oldScopes = []
//
// while( oldDataIterator.hasNext && comparePoints(oldDataIterator.key, to) <= 0 ) {
// this.boundaries = this.boundaries.remove(oldDataIterator.key)
// oldScopes = oldDataIterator.value.closeScopeIds
// oldDataIterator.next()
// }
//
// oldScopes = oldScopes || []
// syntax.forEach(({node, name}) => {
// let id = this.scopeNames.get(name)
// if(!id) {
// this.lastId += 2
// id = this.lastId
// const newId = this.lastId;
// this.scopeNames.set(name, newId)
// this.scopeIds.set(newId, name)
// }
// // })
//
// let old = this.boundaries.get(node.startPosition)
// if(old) {
// old.openNode = node
// if(old.openScopeIds.length === 0) {
// old.openScopeIds = [id]
// }
// } else {
// this.boundaries = this.boundaries.insert(node.startPosition, {
// closeScopeIds: [...oldScopes],
// openScopeIds: [id],
// openNode: node,
// position: node.startPosition
// })
// oldScopes = [id]
// }
//
// old = this.boundaries.get(node.endPosition)
// if(old) {
// old.closeNode = node
// if(old.closeScopeIds.length === 0) old.closeScopeIds = [id]
// } else {
// this.boundaries = this.boundaries.insert(node.endPosition, {
// closeScopeIds: [id],
// openScopeIds: [],
// closeNode: node,
// position: node.endPosition
// })
// }
// })
//
// this.boundaries = this.boundaries.insert(Point.INFINITY, {
// closeScopeIds: [...oldScopes],
// openScopeIds: [],
// position: Point.INFINITY
// })
// }
_prepareInvalidations() {
let nodes = this.oldNodeTexts
let parentScopes = createTree(comparePoints)
@ -299,33 +597,106 @@ class WASMTreeSitterLanguageMode {
classNameForScopeId(scopeId) {
const scope = this.scopeIds.get(scopeId)
if(scope) return `syntax--${scope.replace(/\./g, ' syntax--')}`
if (scope) {
return `syntax--${scope.replace(/\./g, ' syntax--')}`
}
}
scopeForId(scopeId) {
return this.scopeIds[scopeId]
scopeForId (scopeId) {
return this.scopeIds.get(scopeId)
}
scopeDescriptorForPosition(position) {
if(!this.tree) return new ScopeDescriptor({scopes: ['text']})
const current = Point.fromObject(position)
let begin = Point.fromObject(position)
findOrCreateScopeId (name) {
let id = this.scopeNames.get(name)
if (!id) {
this.lastId += 2
id = this.lastId
const newId = this.lastId;
this.scopeNames.set(name, newId)
this.scopeIds.set(newId, name)
}
return id
}
syntaxTreeScopeDescriptorForPosition(point) {
point = this.buffer.clipPosition(Point.fromObject(point));
// If the position is the end of a line, get node of left character instead of newline
// This is to match TextMate behaviour, see https://github.com/atom/atom/issues/18463
if (
point.column > 0 &&
point.column === this.buffer.lineLengthForRow(point.row)
) {
point = point.copy();
point.column--;
}
let scopes = [];
let root = this.tree.rootNode;
let rangeIncludesPoint = (start, end, point) => {
return comparePoints(start, point) <= 0 && comparePoints(end, point) >= 0
};
let iterate = (node, isAnonymous = false) => {
let { startPosition: start, endPosition: end } = node;
if (rangeIncludesPoint(start, end, point)) {
scopes.push(isAnonymous ? `"${node.type}"` : node.type);
let namedChildrenIds = node.namedChildren.map(c => c.typeId);
for (let child of node.children) {
let isAnonymous = !namedChildrenIds.includes(child.typeId);
iterate(child, isAnonymous);
}
}
};
iterate(root);
scopes.unshift(this.grammar.scopeName);
return new ScopeDescriptor({ scopes });
}
scopeDescriptorForPosition (point) {
// If the position is the end of a line, get scope of left character instead of newline
// This is to match TextMate behaviour, see https://github.com/atom/atom/issues/18463
if (
point.column > 0 &&
point.column === this.buffer.lineLengthForRow(point.row)
) {
point = point.copy();
point.column--;
}
if (!this.tree) {
return new ScopeDescriptor({scopes: ['text']})
}
const current = Point.fromObject(point, true)
let begin = Point.fromObject(point, true)
begin.column = 0
const end = Point.fromObject([begin.row+1, 0])
const end = Point.fromObject([begin.row + 1, 0])
this._updateBoundaries(begin, end)
const it = this.boundaries.ge(begin)
if(!it.value) return new ScopeDescriptor({scopes: ['text']})
// Start at the beginning.
const it = this.boundaries.ge(new Point(0, 0))
if (!it.value) {
return new ScopeDescriptor({scopes: ['text']})
}
let scopeIds = []
while(comparePoints(it.key, current) <= 0) {
while (comparePoints(it.key, current) <= 0) {
const closing = new Set(it.value.closeScopeIds)
scopeIds = scopeIds.filter(s => !closing.has(s))
scopeIds.push(...it.value.openScopeIds)
if(!it.hasNext) break
if (!it.hasNext) { break }
it.next()
}
const scopes = scopeIds.map(id => this.classNameForScopeId(id).replace(/^syntax--/, '').replace(/\s?syntax--/g, '.'))
const scopes = scopeIds.map(id => this.scopeForId(id))
if (scopes.length === 0 || scopes[0] !== this.grammar.scopeName) {
scopes.unshift(this.grammar.scopeName);
}
return new ScopeDescriptor({scopes})
}
@ -350,34 +721,83 @@ class WASMTreeSitterLanguageMode {
}
suggestedIndentForBufferRow(row, tabLength, options) {
if(row === 0) return 0;
console.log('suggestedLineForBufferRow', row, tabLength);
if (row === 0) { return 0; }
const lastLineIndent = this.indentLevelForLine(
this.buffer.lineForRow(row - 1), tabLength
)
let amount = lastLineIndent
console.log('going from', row - 1, 'to', row, this.tree.rootNode);
const indents = this.indentsQuery.captures(
this.tree.rootNode,
{row: row-1, column: 0},
{row: row - 1, column: 0},
{row: row, column: 0}
)
const indent = indents.find(i => i.node.startPosition.row === row-1)
const lastLineIndent = this.indentLevelForLine(
this.buffer.getLines()[row-1], tabLength
)
console.log('indents:', indents);
if(indent?.name === 'indent') {
return lastLineIndent + 1
} else {
const suggestion = this.suggestedIndentForEditedBufferRow(row, tabLength)
return suggestion !== undefined ? suggestion : lastLineIndent
let delta = 0;
for (let { name, node } of indents) {
let text = node.text;
if (!text || !text.length) { continue; }
if (name === 'indent') { delta++ }
else if (name === 'indent_end') { delta-- }
console.log('delta is now:', delta);
}
if (delta > 1) { delta = 1; }
if (delta < 0) { delta = 0; }
return lastLineIndent + delta;
}
// suggestedIndentForEditedBufferRow(row, tabLength) {
// if (row === 0) { return 0; }
//
// const indents = this.indentsQuery.captures(
// this.tree.rootNode,
// {row: row, column: 0},
// {row: row + 1, column: 0}
// )
//
// console.log('edited indents:', indents);
//
// let currentLineIndent = this.indentLevelForLine(this.buffer.lineForRow(row), tabLength);
// let originalLineIndent = this.suggestedIndentForBufferRow(row, tabLength);
// // let startingLineIndent = Math.
// console.log('starting at', originalLineIndent);
//
// let delta = 0;
// for (let { name, node } of indents) {
// if (!node.text?.length) { continue; }
//
// if (name === 'branch') {
// delta--
// }
// }
//
// if (delta === 0) {
// return currentLineIndent;
// }
//
// if (delta < -1) { delta = -1; }
// return Math.max(0, originalLineIndent + delta);
// }
suggestedIndentForEditedBufferRow(row, tabLength) {
const indents = this.indentsQuery.captures(
this.tree.rootNode,
{row: row, column: 0},
{row: row+1, column: 0}
)
const indent = indents.find(i => i.node.startPosition.row === row)
if(indent?.name === "indent_end") {
if(this.buffer.getLines()[row].trim() === indent.node.text) {
console.log('indents:', indents);
const indent = indents.find(i => {
return i.node.startPosition.row === row && i.name === 'branch'
});
console.log('specific indent:', indent);
if(indent?.name === "branch") {
if(this.buffer.lineForRow(row).trim() === indent.node.text) {
const parent = indent.node.parent
if(parent) return this.indentLevelForLine(
this.buffer.getLines()[parent.startPosition.row],
@ -386,7 +806,6 @@ class WASMTreeSitterLanguageMode {
}
}
}
// Copied from original tree-sitter. I honestly didn't even read this.
indentLevelForLine(line, tabLength) {
let indentLength = 0;
@ -424,9 +843,12 @@ class WASMTreeSitterLanguageMode {
}
_getFoldsAtRow(row) {
if(!this.tree) return []
const folds = this.foldsQuery.captures(this.tree.rootNode,
{row: row, column: 0}, {row: row+1, column: 0})
if (!this.tree) { return [] }
const folds = this.foldsQuery.captures(
this.tree.rootNode,
{ row: row, column: 0 },
{ row: row + 1, column: 0 }
)
return folds.filter(fold => fold.node.startPosition.row === row)
}
}
@ -467,3 +889,10 @@ function comparePoints(a, b) {
else
return rows
}
function isBetweenPoints (point, a, b) {
let comp = comparePoints(a, b);
let lesser = comp > 0 ? b : a;
let greater = comp > 0 ? a : b;
return comparePoints(point, lesser) >= 0 && comparePoints(point, greater) <= 0;
}