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;