mirror of
https://github.com/pulsar-edit/pulsar.git
synced 2024-08-16 14:40:23 +03:00
Merge pull request #906 from savetheclocktower/tree-sitter-february
Tree-sitter rolling fixes (February)
This commit is contained in:
commit
ec12b2f0af
@ -1 +1,2 @@
|
||||
*.ts
|
||||
vendor
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
;
|
||||
; <div
|
||||
; foo="bar"
|
||||
; baz="thud">
|
||||
;
|
||||
; </div>
|
||||
;
|
||||
; 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…
|
||||
;
|
||||
; <div
|
||||
; foo="bar"
|
||||
; baz="thud"
|
||||
; >
|
||||
;
|
||||
; </div>
|
||||
;
|
||||
; …we can put a fold indicator on the line with `<div` and use it to fold up
|
||||
; all of a start tag's attributes.
|
||||
;
|
||||
; We keep the end of the fold on a separate line because otherwise we lose the
|
||||
; ability to independently toggle the folding of 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 lastChild.startPosition)
|
||||
(#set! fold.adjustToEndOfPreviousRow true))
|
||||
|
||||
|
||||
[
|
||||
(element)
|
||||
(script_element)
|
||||
(style_element)
|
||||
] @fold
|
||||
|
@ -1,7 +1,7 @@
|
||||
|
||||
((start_tag) @indent
|
||||
; Only indent if this isn't a self-closing tag.
|
||||
(#not-match? @indent "^<(?:area|base|br|col|embed|hr|img|input|keygen|link|meta|param|source|track|wbr)\\s"))
|
||||
(#not-match? @indent "^<(?:area|base|br|col|embed|hr|img|input|keygen|link|meta|param|source|track|wbr)(?=\\s|>)"))
|
||||
|
||||
; `end_tag` will still match when only `</div` is present. Without enforcing
|
||||
; the presence of `>`, the dedent happens too soon.
|
||||
|
@ -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'
|
||||
|
@ -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 "^/\\*(?!\\*)"))
|
||||
|
||||
|
@ -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)
|
||||
|
Binary file not shown.
@ -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
|
||||
; =========
|
||||
|
||||
|
@ -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`
|
||||
<div
|
||||
foo="bar">
|
||||
<span>hello</span>
|
||||
<span>world</span>
|
||||
</div>
|
||||
`);
|
||||
|
||||
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`
|
||||
<div
|
||||
foo="bar"
|
||||
>
|
||||
<span>hello</span>
|
||||
<span>world</span>
|
||||
</div>
|
||||
`)
|
||||
|
||||
// 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`
|
||||
<img
|
||||
foo="bar"
|
||||
baz="thud"
|
||||
troz="zort"
|
||||
>
|
||||
|
||||
<div>
|
||||
<img
|
||||
foo="bar"
|
||||
baz="thud"
|
||||
troz="zort"
|
||||
>
|
||||
</div>
|
||||
`);
|
||||
|
||||
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);
|
||||
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
});
|
||||
|
@ -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));
|
||||
|
@ -42,3 +42,5 @@
|
||||
@syntax-color-attribute: #87400d;
|
||||
@syntax-color-import: #97C378;
|
||||
@syntax-color-snippet: #97C378;
|
||||
@syntax-color-string: #97C378;
|
||||
@syntax-color-comment: #888;
|
||||
|
Loading…
Reference in New Issue
Block a user