diff --git a/.eslintignore b/.eslintignore index 6461deecd..cd878a46f 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1 +1,2 @@ *.ts +vendor diff --git a/packages/language-c/grammars/tree-sitter-c/highlights.scm b/packages/language-c/grammars/tree-sitter-c/highlights.scm index 3a2db6c79..344340d1e 100644 --- a/packages/language-c/grammars/tree-sitter-c/highlights.scm +++ b/packages/language-c/grammars/tree-sitter-c/highlights.scm @@ -1,3 +1,4 @@ + ; PREPROCESSOR ; ============ @@ -16,21 +17,48 @@ (["#if" "#ifdef" "#ifndef" "#endif" "#elif" "#else" "#define" "#include"] @punctuation.definition.directive.c (#set! adjust.endAfterFirstMatchOf "^#")) - -; This will match if the more specific rules above haven't matched. The -; anonymous nodes will match under ideal conditions, but might not be present -; if the parser is flummoxed. +; `preproc_directive` will be used when the parser doesn't recognize the +; directive as one of the above. It's permissive; `#afdfafsdfdfad` would be +; parsed as a `preproc_directive`. +; +; Hence this rule will match if the more specific rules above haven't matched. +; The anonymous nodes will match under ideal conditions, but might not be +; present even when they ought to be _if_ the parser is flummoxed; so this'll +; sometimes catch `#ifdef` and others. ((preproc_directive) @keyword.control.directive.c (#set! capture.shy true)) -((preproc_ifdef - (identifier) @entity.name.function.preprocessor.c - (#match? @entity.name.function.preprocessor.c "[a-zA-Z_$][\\w$]*"))) +((preproc_directive) @punctuation.definition.directive.c + (#set! capture.shy true) + (#set! adjust.endAfterFirstMatchOf "^#")) +; Macro functions are definitely entities. (preproc_function_def (identifier) @entity.name.function.preprocessor.c (#set! capture.final true)) +; Identifiers in macro definitions are definitely constants. +((preproc_def + name: (identifier) @constant.preprocessor.c)) + +; We can also safely treat identifiers as constants in `#ifdef`… +((preproc_ifdef + (identifier) @constant.preprocessor.c)) + +; …and `#if` and `#elif`… +(preproc_if + (binary_expression + (identifier) @constant.preprocessor.c)) +(preproc_elif + (binary_expression + (identifier) @constant.preprocessor.c)) + +; …and `#undef`. +((preproc_call + directive: (preproc_directive) @_IGNORE_ + argument: (preproc_arg) @constant.preprocessor.c) + (#eq? @_IGNORE_ "#undef")) + (system_lib_string) @string.quoted.other.lt-gt.include.c ((system_lib_string) @punctuation.definition.string.begin.c (#set! adjust.endAfterFirstMatchOf "^<")) @@ -48,6 +76,15 @@ (#set! capture.final true)) (primitive_type) @support.storage.type.builtin.c + +; When the user has typed `#define FOO`, the macro injection thinks that `FOO` +; is a type declaration (for some reason). This node structure seems to exist +; only in that unusual and incorrect scenario, so we'll stop it from happening +; so that it doesn't override the underlying `constant.other.c` scope. +(translation_unit + (type_identifier) @_IGNORE_ + (#set! capture.final)) + (type_identifier) @support.other.storage.type.c ; These types are all reserved words; if we see an identifier with this name, @@ -133,27 +170,31 @@ ; The "x" in `int x;` (declaration - declarator: (identifier) @variable.declaration.c) + declarator: (identifier) @variable.other.declaration.c) ; The "x" in `int x = y;` (init_declarator - declarator: (identifier) @variable.declaration.c) + declarator: (identifier) @variable.other.declaration.c) ; The "x" in `SomeType *x;` ; (Should work no matter how many pointers deep we are.) (pointer_declarator - declarator: [(identifier) (field_identifier)] @variable.declaration.pointer.c + declarator: [(identifier) (field_identifier)] @variable.other.declaration.pointer.c (#is? test.descendantOfType "declaration field_declaration")) +; An array declarator: the "table" in `int table[4];` +(array_declarator + declarator: (identifier) @variable.other.declaration.c) + ; A member of a struct. (field_declaration - (field_identifier) @variable.declaration.member.c) + (field_identifier) @variable.other.declaration.member.c) ; An attribute in a C99 struct designated initializer: ; the "foo" in `MY_TYPE a = { .foo = true }; (initializer_pair (field_designator - (field_identifier) @variable.declaration.member.c)) + (field_identifier) @variable.other.declaration.member.c)) ; (and the associated ".") (initializer_pair @@ -162,15 +203,15 @@ (field_declaration (pointer_declarator - (field_identifier) @variable.declaration.member.c)) + (field_identifier) @variable.other.declaration.member.c)) (field_declaration (array_declarator - (field_identifier) @variable.declaration.member.c)) + (field_identifier) @variable.other.declaration.member.c)) (init_declarator (pointer_declarator - (identifier) @variable.declaration.member.c)) + (identifier) @variable.other.declaration.member.c)) ; The "x" in `x = y;` (assignment_expression @@ -253,8 +294,19 @@ (false) ] @constant.language._TYPE_.c -((identifier) @constant.c - (#match? @constant.c "[_A-Z][_A-Z0-9]*$")) +; Don't try to scope (e.g.) `int FOO = 1` as a constant when the user types `=` +; but has not typed the value yet. +(ERROR + (identifier) @_IGNORE_ + (#set! capture.final)) + +; In most languages we wouldn't be making the assumption that an all-caps +; identifier should be treated as a constant. But those languages don't have +; macro preprocessors. The convention is decently strong in C/C++ that all-caps +; identifiers will refer to `#define`d things. +((identifier) @constant.other.c + (#match? @constant.other.c "^[_A-Z][_A-Z0-9]*$") + (#set! capture.shy)) ; COMMENTS diff --git a/packages/language-c/grammars/tree-sitter-cpp/highlights.scm b/packages/language-c/grammars/tree-sitter-cpp/highlights.scm index bc3fc2c3c..ef65e50a4 100644 --- a/packages/language-c/grammars/tree-sitter-cpp/highlights.scm +++ b/packages/language-c/grammars/tree-sitter-cpp/highlights.scm @@ -13,33 +13,55 @@ "#define" @keyword.control.directive.define.cpp "#include" @keyword.control.directive.include.cpp -(["#if" "#ifdef" "#ifndef" "#endif" "#elif" "#else" "#define" "#include"] @punctuation.definition.directive.c +(["#if" "#ifdef" "#ifndef" "#endif" "#elif" "#else" "#define" "#include"] @punctuation.definition.directive.cpp (#set! adjust.endAfterFirstMatchOf "^#")) - -; This will match if the more specific rules above haven't matched. The -; anonymous nodes will match under ideal conditions, but might not be present -; if the parser is flummoxed. -((preproc_directive) @keyword.control.directive.c +; `preproc_directive` will be used when the parser doesn't recognize the +; directive as one of the above. It's permissive; `#afdfafsdfdfad` would be +; parsed as a `preproc_directive`. +; +; Hence this rule will match if the more specific rules above haven't matched. +; The anonymous nodes will match under ideal conditions, but might not be +; present even when they ought to be _if_ the parser is flummoxed; so this'll +; sometimes catch `#ifdef` and others. +((preproc_directive) @keyword.control.directive.cpp (#set! capture.shy true)) -((preproc_ifdef - (identifier) @entity.name.function.preprocessor.c - (#match? @entity.name.function.preprocessor.c "[a-zA-Z_$][\\w$]*"))) - -(preproc_function_def - (identifier) @entity.name.function.preprocessor.c - (#set! capture.final true)) +((preproc_directive) @punctuation.definition.directive.cpp + (#set! capture.shy true) + (#set! adjust.endAfterFirstMatchOf "^#")) +; Macro functions are definitely entities. (preproc_function_def (identifier) @entity.name.function.preprocessor.cpp - (#set! capture.final true) -) + (#set! capture.final true)) -(system_lib_string) @string.quoted.other.lt-gt.include.c -((system_lib_string) @punctuation.definition.string.begin.c +; Identifiers in macro definitions are definitely constants. +((preproc_def + name: (identifier) @constant.preprocessor.cpp)) + +; We can also safely treat identifiers as constants in `#ifdef`… +((preproc_ifdef + (identifier) @constant.preprocessor.cpp)) + +; …and `#if` and `#elif`… +(preproc_if + (binary_expression + (identifier) @constant.preprocessor.cpp)) +(preproc_elif + (binary_expression + (identifier) @constant.preprocessor.cpp)) + +; …and `#undef`. +((preproc_call + directive: (preproc_directive) @_IGNORE_ + argument: (preproc_arg) @constant.preprocessor.cpp) + (#eq? @_IGNORE_ "#undef")) + +(system_lib_string) @string.quoted.other.lt-gt.include.cpp +((system_lib_string) @punctuation.definition.string.begin.cpp (#set! adjust.endAfterFirstMatchOf "^<")) -((system_lib_string) @punctuation.definition.string.end.c +((system_lib_string) @punctuation.definition.string.end.cpp (#set! adjust.startBeforeFirstMatchOf ">$")) @@ -52,6 +74,13 @@ (type_identifier) @_IGNORE_ (#set! capture.final true)) +; When the user has typed `#define FOO`, the macro injection thinks that `FOO` +; is a type declaration (for some reason). This node structure seems to exist +; only in that unusual and incorrect scenario, so we'll stop it from happening +; so that it doesn't override the underlying `constant.other.c` scope. +(translation_unit + (type_identifier) @_IGNORE_ + (#set! capture.final)) (primitive_type) @support.type.builtin.cpp @@ -232,7 +261,7 @@ ; The "x" in `SomeType *x;` ; (Should work no matter how many pointers deep we are.) (pointer_declarator - declarator: [(identifier) (field_identifier)] @variable.declaration.pointer.c + declarator: [(identifier) (field_identifier)] @variable.declaration.pointer.cpp (#is? test.descendantOfType "declaration field_declaration")) ; A member of a struct. @@ -289,7 +318,7 @@ ; The "foo" in `const char *foo` within a parameter list. ; (Should work no matter how many pointers deep we are.) (pointer_declarator - declarator: [(identifier) (field_identifier)] @variable.parameter.pointer.c + declarator: [(identifier) (field_identifier)] @variable.parameter.pointer.cpp (#is? test.descendantOfType "parameter_declaration")) (parameter_declaration @@ -332,8 +361,19 @@ (false) ] @constant.language._TYPE_.cpp -((identifier) @constant.cpp - (#match? @constant.cpp "[_A-Z][_A-Z0-9]*$")) +; Don't try to scope (e.g.) `int FOO = 1` as a constant when the user types `=` +; but has not typed the value yet. +(ERROR + (identifier) @_IGNORE_ + (#set! capture.final)) + +; In most languages we wouldn't be making the assumption that an all-caps +; identifier should be treated as a constant. But those languages don't have +; macro preprocessors. The convention is decently strong in C/C++ that all-caps +; identifiers will refer to `#define`d things. +((identifier) @constant.other.cpp + (#match? @constant.other.cpp "[_A-Z][_A-Z0-9]*$") + (#set! capture.shy)) ; COMMENTS diff --git a/packages/language-html/grammars/tree-sitter-html/folds.scm b/packages/language-html/grammars/tree-sitter-html/folds.scm index 5c8919ff7..bc80d5ff6 100644 --- a/packages/language-html/grammars/tree-sitter-html/folds.scm +++ b/packages/language-html/grammars/tree-sitter-html/folds.scm @@ -1,6 +1,74 @@ +; When dealing with a self-closing element that spans multiple lines, this lets +; us fold the attribute list. +; +; This query captures elements that happen to be self-closing but don't end +; with an XHTML-style ` />`. Because `tree-sitter-html` doesn't distinguish +; these from elements that can have content, we have to check the tag name to +; know how to treat these. + +((element + (start_tag + (tag_name) @_IGNORE_) @fold) + (#match? @_IGNORE_ "^(area|base|br|col|embed|hr|img|input|keygen|link|meta|param|source|track|wbr)$") +) + +; This one captures the XHTML-style nodes. +(self_closing_tag) @fold + + +; TODO: Right now, the fold cache doesn't work properly when a given range +; satisfies more than one fold. We should employ `ScopeResolver` to fix this. + +; Fold up all of +; +;
+; +;
+; +; with the fold indicator appearing on whichever line has the `>` that closes +; the opening tag. +; +; Usually this'll be the same line on which the tag opened; but when it isn't, +; this allows for the attribute list of the opening element to be folded +; separately from the element's contents. +; + +(element + (start_tag + (tag_name) @_IGNORE_ + ">" @fold) + (#not-match? @_IGNORE_ "^(area|base|br|col|embed|hr|img|input|keygen|link|meta|param|source|track|wbr)$") + (#set! fold.endAt parent.parent.lastNamedChild.startPosition) + (#set! fold.adjustToEndOfPreviousRow true) +) + + +; When we have… +; +;
+; +;
+; +; …we can put a fold indicator on the line with `)")) ; `end_tag` will still match when only ``, the dedent happens too soon. diff --git a/packages/language-php/grammars/modern-tree-sitter-phpdoc.cson b/packages/language-php/grammars/modern-tree-sitter-phpdoc.cson index a3a43b1c7..0bb2bde06 100644 --- a/packages/language-php/grammars/modern-tree-sitter-phpdoc.cson +++ b/packages/language-php/grammars/modern-tree-sitter-phpdoc.cson @@ -6,6 +6,6 @@ parser: 'tree-sitter-phpdoc' injectionRegex: '^(phpdoc|PHPDoc)$' treeSitter: - parserSource: 'github:claytonrcarter/tree-sitter-phpdoc#915a527d5aafa81b31acf67fab31b0ac6b6319c0' + parserSource: 'github:claytonrcarter/tree-sitter-phpdoc#f285e338d328a03920a9bfd8dda78585c7ddcca3' grammar: 'tree-sitter/tree-sitter-phpdoc.wasm' highlightsQuery: 'tree-sitter/queries/phpdoc/highlights.scm' diff --git a/packages/language-php/grammars/tree-sitter/queries/highlights.scm b/packages/language-php/grammars/tree-sitter/queries/highlights.scm index 0af6dde5d..cd3ab01e7 100644 --- a/packages/language-php/grammars/tree-sitter/queries/highlights.scm +++ b/packages/language-php/grammars/tree-sitter/queries/highlights.scm @@ -511,16 +511,22 @@ (#match? @punctuation.definition.comment.php "^#") (#set! adjust.startAndEndAroundFirstMatchOf "^#")) +; All block comments get re-highlighted whenever a change takes place inside +; them. +((comment) @_IGNORE_ + (#match? @_IGNORE_ "^/\\*") + (#set! highlight.invalidateOnChange true)) + ; Capture these because the PHPDoc injection won't process them… ((comment) @comment.block.documentation.php - (#match? @comment.block.documentation.php "^/\\*\\*\\*")) + (#match? @comment.block.documentation.php "^/\\*\\*\\*") + (#set! highlight.invalidateOnChange true)) ; …but otherwise leave this style of comment to be handled by PHPDoc. ((comment) @_IGNORE_ (#match? @_IGNORE_ "^/\\*\\*") (#set! capture.final true)) - ((comment) @comment.block.php (#match? @comment.block.php "^/\\*(?!\\*)")) diff --git a/packages/language-php/grammars/tree-sitter/queries/phpdoc/highlights.scm b/packages/language-php/grammars/tree-sitter/queries/phpdoc/highlights.scm index dfb4fee84..d064e4fa6 100644 --- a/packages/language-php/grammars/tree-sitter/queries/phpdoc/highlights.scm +++ b/packages/language-php/grammars/tree-sitter/queries/phpdoc/highlights.scm @@ -13,3 +13,7 @@ (inline_tag "{" @punctation.definition.tag.begin.brace.curly.phpdoc.php) (inline_tag "}" @punctation.definition.tag.end.brace.curly.phpdoc.php) + +(array_type "<" @punctuation.definition.generic.begin.bracket.angle.phpdoc.php) +(array_type ">" @punctuation.definition.generic.end.bracket.angle.phpdoc.php) +(array_type "," @punctuation.separator.generic.comma.phpdoc.php) diff --git a/packages/language-php/grammars/tree-sitter/tree-sitter-phpdoc.wasm b/packages/language-php/grammars/tree-sitter/tree-sitter-phpdoc.wasm index 0799df476..321ff55c8 100755 Binary files a/packages/language-php/grammars/tree-sitter/tree-sitter-phpdoc.wasm and b/packages/language-php/grammars/tree-sitter/tree-sitter-phpdoc.wasm differ diff --git a/packages/language-typescript/grammars/common/highlights.scm b/packages/language-typescript/grammars/common/highlights.scm index 0e78ab2dd..ddaf57438 100644 --- a/packages/language-typescript/grammars/common/highlights.scm +++ b/packages/language-typescript/grammars/common/highlights.scm @@ -295,6 +295,21 @@ name: (_) @entity.name.type.interface._LANG_ (#set! capture.final)) +; ENUMS +; ===== + +; The "Foo" in `enum Foo {` +(enum_declaration + name: (_) @entity.name.type.enum._LANG_ + (#set! capture.final)) + +; The "foo" and "bar" in `enum Baz { foo, bar }` +(enum_body + name: (property_identifier) @variable.declaration.enum._LANG_) + +; The "foo" in `enum Bar { foo = 1 }` +(enum_assignment + name: (property_identifier) @variable.declaration.enum._LANG_) ; TYPES ; ===== @@ -726,6 +741,13 @@ "}" @punctuation.definition.template-expression.end._LANG_ ) @meta.embedded.line.interpolation._LANG_ +(string + (escape_sequence) @constant.character.escape.js) + +(template_string + (escape_sequence) @constant.character.escape.js) + + ; CONSTANTS ; ========= diff --git a/spec/wasm-tree-sitter-language-mode-spec.js b/spec/wasm-tree-sitter-language-mode-spec.js index 4a9ddf061..60b3f056b 100644 --- a/spec/wasm-tree-sitter-language-mode-spec.js +++ b/spec/wasm-tree-sitter-language-mode-spec.js @@ -17,6 +17,11 @@ function resolve(modulePath) { return require.resolve(`${PATH}/${modulePath}`) } +// Just for syntax highlighting. +function scm(strings) { + return strings.join(''); +} + const cGrammarPath = resolve('language-c/grammars/modern-tree-sitter-c.cson'); const pythonGrammarPath = resolve( 'language-python/grammars/modern-tree-sitter-python.cson' @@ -1761,20 +1766,6 @@ describe('WASMTreeSitterLanguageMode', () => { ] @fold `); - // { - // parser: 'tree-sitter-javascript', - // folds: [ - // { - // start: { type: '{', index: 0 }, - // end: { type: '}', index: -1 } - // }, - // { - // start: { type: '(', index: 0 }, - // end: { type: ')', index: -1 } - // } - // ] - // } - buffer.setText(dedent` module.exports = class A { @@ -1936,6 +1927,121 @@ describe('WASMTreeSitterLanguageMode', () => { `); }); + it('updates its fold cache properly when `fold.invalidateOnChange` is specified', async () => { + const grammar = new WASMTreeSitterGrammar(atom.grammars, htmlGrammarPath, htmlConfig); + + await grammar.setQueryForTest('foldsQuery', scm` + ((element + (start_tag + (tag_name) @_IGNORE_) @fold) + (#match? @_IGNORE_ "^(area|base|br|col|embed|hr|img|input|keygen|link|meta|param|source|track|wbr)$") + (#set! fold.invalidateOnChange true) + ) + + (element + (start_tag + (tag_name) @_IGNORE_ + ">" @fold) + (#not-match? @_IGNORE_ "^(area|base|br|col|embed|hr|img|input|keygen|link|meta|param|source|track|wbr)$") + (#set! fold.endAt parent.parent.lastNamedChild.startPosition) + (#set! fold.adjustToEndOfPreviousRow true) + ) + + (element + (start_tag + (tag_name) @_IGNORE_) @fold + (#not-match? @_IGNORE_ "^(area|base|br|col|embed|hr|img|input|keygen|link|meta|param|source|track|wbr)$") + (#set! fold.invalidateOnChange true) + (#set! fold.endAt lastChild.startPosition) + (#set! fold.adjustToEndOfPreviousRow true)) + `); + + buffer.setText(dedent` +
+ hello + world +
+ `); + + const languageMode = new WASMTreeSitterLanguageMode({ grammar, buffer }); + buffer.setLanguageMode(languageMode); + await languageMode.ready; + + expect(editor.isFoldableAtBufferRow(0)).toBe(false); + expect(editor.isFoldableAtBufferRow(1)).toBe(true); + expect(editor.isFoldableAtBufferRow(2)).toBe(false); + expect(editor.isFoldableAtBufferRow(3)).toBe(false); + expect(editor.isFoldableAtBufferRow(4)).toBe(false); + + editor.setCursorBufferPosition([1, 11]); + editor.insertText('\n'); + await languageMode.atTransactionEnd(); + + expect(editor.getText()).toBe(dedent` +
+ hello + world +
+ `) + + // Making that buffer change on line 1 should invalidate the fold cache + // on line 0. + expect(editor.isFoldableAtBufferRow(0)).toBe(true); + expect(editor.isFoldableAtBufferRow(1)).toBe(false); + expect(editor.isFoldableAtBufferRow(2)).toBe(true); + expect(editor.isFoldableAtBufferRow(3)).toBe(false); + expect(editor.isFoldableAtBufferRow(4)).toBe(false); + }); + + it('understands custom predicates', async () => { + const grammar = new WASMTreeSitterGrammar(atom.grammars, htmlGrammarPath, htmlConfig); + + await grammar.setQueryForTest('foldsQuery', scm` + ((element + (start_tag + (tag_name) @_IGNORE_.tag)) @_IGNORE_.element + (#eq? @_IGNORE_.tag "div") + (#set! isDiv true)) + + ; Make self-closing elements foldable only when they're ancestors of + ; DIVs. This is a very silly thing to do. + ((element + (start_tag + (tag_name) @_IGNORE_) @fold) + (#match? @_IGNORE_ "^(area|base|br|col|embed|hr|img|input|keygen|link|meta|param|source|track|wbr)$") + (#set! test.descendantOfNodeWithData "isDiv") + (#set! capture.final) + ) + + `); + + buffer.setText(dedent` + + +
+ +
+ `); + + const languageMode = new WASMTreeSitterLanguageMode({ grammar, buffer }); + buffer.setLanguageMode(languageMode); + await languageMode.ready; + + expect(editor.isFoldableAtBufferRow(0)).toBe(false); + expect(editor.isFoldableAtBufferRow(7)).toBe(true); + }); + it('can fold entire nodes when no start or end parameters are specified', async () => { const grammar = new WASMTreeSitterGrammar(atom.grammars, jsGrammarPath, jsConfig); diff --git a/src/scope-resolver.js b/src/scope-resolver.js index 1760e4a9d..bd89e086e 100644 --- a/src/scope-resolver.js +++ b/src/scope-resolver.js @@ -196,6 +196,11 @@ class ScopeResolver { ('highlight.invalidateOnChange' in capture.setProperties); } + shouldInvalidateFoldOnChange(capture) { + return capture.setProperties && + ('fold.invalidateOnChange' in capture.setProperties); + } + // We want to index scope data on buffer position, but each `Point` (or // ad-hoc point object) is a different object. We could normalize them to a // string and use the string as the map key, but we'd have to convert them diff --git a/src/task.js b/src/task.js index a20e1f226..8cbfb43ec 100644 --- a/src/task.js +++ b/src/task.js @@ -73,9 +73,9 @@ module.exports = class Task { const env = Object.assign({}, process.env, {userAgent: navigator.userAgent}); this.childProcess = ChildProcess.fork(require.resolve('./task-bootstrap'), [compileCachePath, taskPath], { env, silent: true}); - this.on("task:log", () => console.log(...arguments)); - this.on("task:warn", () => console.warn(...arguments)); - this.on("task:error", () => console.error(...arguments)); + this.on("task:log", (...args) => console.log(...args) ); + this.on("task:warn", (...args) => console.warn(...args) ); + this.on("task:error", (...args) => console.error(...args)); this.on("task:deprecations", (deprecations) => { for (let i = 0; i < deprecations.length; i++) { @@ -157,7 +157,7 @@ module.exports = class Task { } once(eventName, callback) { - var disposable = this.on(eventName, function(...args) { + var disposable = this.on(eventName, function (...args) { disposable.dispose(); callback(...args); }); diff --git a/src/wasm-tree-sitter-language-mode.js b/src/wasm-tree-sitter-language-mode.js index e7e54df13..183a48187 100644 --- a/src/wasm-tree-sitter-language-mode.js +++ b/src/wasm-tree-sitter-language-mode.js @@ -343,13 +343,22 @@ class WASMTreeSitterLanguageMode { }); } - emitRangeUpdate(range) { + // Invalidate fold caches for the rows touched by the given range. + // + // Invalidating syntax highlighting also invalidates fold caches for the same + // range, but this method allows us to invalidate parts of the fold cache + // without affecting syntax highlighting. + emitFoldUpdate(range) { const startRow = range.start.row; const endRow = range.end.row; for (let row = startRow; row < endRow; row++) { this.isFoldableCache[row] = undefined; } this.prefillFoldCache(range); + } + + emitRangeUpdate(range) { + this.emitFoldUpdate(range); this.emitter.emit('did-change-highlighting', range); } @@ -2137,11 +2146,9 @@ class FoldResolver { return result; } - // The red-black tree we use here is a bit more complex up front than the - // one we use for syntax boundaries, because I didn't want the added - // complexity later on of having to aggregate boundaries when they share a - // position in the buffer. - // + let scopeResolver = this.layer.scopeResolver; + scopeResolver.reset(); + // Instead of keying off of a plain buffer position, this tree also // considers whether the boundary is a fold start or a fold end. If one // boundary ends at the same point that another one starts, the ending @@ -2150,18 +2157,44 @@ class FoldResolver { let captures = this.layer.foldsQuery.captures(rootNode, start, end); for (let capture of captures) { - if (capture.node.startPosition.row < start.row) { continue; } + // NOTE: Currently, the first fold to match for a given starting position + // is the only one considered. That's because we use a version of a + // red-black tree in which we silently ignore any attempts to add a key + // that is equivalent in value to that of a previously added key. + // + // Attempts to use `capture.final` and `capture.shy` won't harm anything, + // but they'll be redundant. Other types of custom predicates, however, + // should work just fine. + let result = scopeResolver.store(capture); + if (!result) { continue; } + + // Some folds are unusual enough that they can flip from valid to + // invalid, or vice versa, based on edits to rows other than their + // starting row. We need to keep track of these nodes so that we can + // invalidate the fold cache properly when edits happen inside of them. + if (scopeResolver.shouldInvalidateFoldOnChange(capture)) { + this.layer.foldNodesToInvalidateOnChange.add(capture.node.id); + } + + if (capture.node.startPosition.row < start.row) { + // This fold starts before the range we're interested in. We needed to + // run these nodes through the scope resolver for various reasons, but + // they're not relevant to our iterator. + continue; + } if (capture.name === 'fold') { boundaries = boundaries.insert({ position: capture.node.startPosition, boundary: 'start' }, capture); - } else { + } else if (capture.name.startsWith('fold.')) { let key = this.keyForDividedFold(capture); boundaries = boundaries.insert(key, capture); } } + scopeResolver.reset(); + this.boundaries = boundaries; this.boundariesRange = new Range(start, end); @@ -2956,6 +2989,7 @@ class LanguageLayer { this.rangeList = new RangeList(); this.nodesToInvalidateOnChange = new Set(); + this.foldNodesToInvalidateOnChange = new Set(); this.tree = null; this.lastSyntaxTree = null; @@ -3110,6 +3144,7 @@ class LanguageLayer { let range = this.getExtent(); this.languageMode.emitRangeUpdate(range); this.nodesToInvalidateOnChange.clear(); + this.foldNodesToInvalidateOnChange.clear(); this._pendingQueryFileChange = false; } catch (error) { console.error(`Error parsing query file: ${queryType}`); @@ -3597,6 +3632,32 @@ class LanguageLayer { return { scopes, definitions, references }; } + // Given a range and a `Set` of node IDs, test if any of those nodes' ranges + // overlap with the given range. + // + // We use this to test if a given edit should trigger the behavior indicated + // by `(fold|highlight).invalidateOnChange`. + searchForNodesInRange(range, nodeIdSet) { + let node = this.getSyntaxNodeContainingRange( + range, + n => nodeIdSet.has(n.id) + ); + + if (node) { + // One of this node's ancestors might also be in our list, so we'll + // traverse upwards and find out. + let ancestor = node.parent; + while (ancestor) { + if (nodeIdSet.has(ancestor.id)) { + node = ancestor; + } + ancestor = ancestor.parent; + } + return node; + } + return null; + } + async _performUpdate(nodeRangeSet, params = {}) { // It's much more common in specs than in real life, but it's always // possible for a layer to get destroyed during the async period between @@ -3664,31 +3725,37 @@ class LanguageLayer { this.lastTransactionEditedRange = this.editedRange; this.editedRange = null; + let foldRangeList = new RangeList(); + // Look for a node that was marked with `invalidateOnChange`. If we find // one, we should invalidate that node's entire buffer region. if (affectedRange) { - let node = this.getSyntaxNodeContainingRange( + + // First look for nodes that were previously marked with + // `highlight.invalidateOnChange`; those will specify ranges for which + // we'll need to force a re-highlight. + let node = this.searchForNodesInRange( affectedRange, - n => this.nodesToInvalidateOnChange.has(n.id) + this.nodesToInvalidateOnChange ); - if (node) { - // One of this node's ancestors might also be in our invalidation list, - // so we'll traverse upwards to see if we should invalidate a larger - // node instead. - let ancestor = node.parent; - while (ancestor) { - if (this.nodesToInvalidateOnChange.has(ancestor.id)) { - node = ancestor; - } - ancestor = ancestor.parent; - } - this.rangeList.add(node.range); } + + // Now look for nodes that were previously marked with + // `fold.invalidateOnChange`; those will specify ranges that need their + // fold cache updated even when highlighting is unaffected. + let foldNode = this.searchForNodesInRange( + affectedRange, + this.foldNodesToInvalidateOnChange + ); + if (foldNode) { + foldRangeList.add(foldNode.range); + } } this.nodesToInvalidateOnChange.clear(); + this.foldNodesToInvalidateOnChange.clear(); if (this.lastSyntaxTree) { const rangesWithSyntaxChanges = this.lastSyntaxTree.getChangedRanges(tree); @@ -3762,6 +3829,13 @@ class LanguageLayer { this.languageMode.emitRangeUpdate(range); } + for (let range of foldRangeList) { + // The fold cache is automatically cleared for any range that needs + // re-highlighting. But sometimes we need to go further and invalidate + // rows that don't even need highlighting changes. + this.languageMode.emitFoldUpdate(range); + } + if (affectedRange) { let injectionPromise = this._populateInjections(affectedRange, nodeRangeSet); if (injectionPromise) { @@ -3795,6 +3869,9 @@ class LanguageLayer { return markers.map(m => m.getRange()); } + // Checks whether a given {Point} lies within one of this layer's content + // ranges — not just its extent. The optional `exclusive` flag will return + // `false` if the point lies on a boundary of a content range. containsPoint(point, exclusive = false) { let ranges = this.getCurrentRanges() ?? [this.getExtent()]; return ranges.some(r => r.containsPoint(point, exclusive)); diff --git a/static/variables/syntax-variables.less b/static/variables/syntax-variables.less index 23b8d994e..a629f0aae 100644 --- a/static/variables/syntax-variables.less +++ b/static/variables/syntax-variables.less @@ -42,3 +42,5 @@ @syntax-color-attribute: #87400d; @syntax-color-import: #97C378; @syntax-color-snippet: #97C378; +@syntax-color-string: #97C378; +@syntax-color-comment: #888;