diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 000000000..6461deecd --- /dev/null +++ b/.eslintignore @@ -0,0 +1 @@ +*.ts diff --git a/.eslintrc.js b/.eslintrc.js index 520c2aeb7..08b4fff87 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -19,6 +19,12 @@ module.exports = { asyncArrow: "always", named: "never" }], + "node/no-missing-require": [ + "error", + { + allowModules: ["atom"] + } + ], "node/no-unpublished-require": [ "error", { diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1a25cc86a..277496c14 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -208,7 +208,8 @@ jobs: node ./rolling-release-binary-upload.js - name: Upload Video Artifacts - if: runner.os != 'Linux' + # Run whether this job passed or failed, unless explicitly cancelled. + if: ${{ !cancelled() && runner.os != 'Linux' }} uses: actions/upload-artifact@v3 with: name: ${{ matrix.os }} Videos @@ -267,7 +268,8 @@ jobs: node ./rolling-release-binary-upload.js - name: Upload Video Artifacts - Linux - if: runner.os == 'Linux' + # Run whether this job passed or failed, unless explicitly cancelled. + if: ${{ !cancelled() && runner.os == 'Linux' }} uses: actions/upload-artifact@v3 with: name: ${{ matrix.os }} Videos diff --git a/integration/workspace.spec.js b/integration/workspace.spec.js index 67c53c07e..10c49797a 100644 --- a/integration/workspace.spec.js +++ b/integration/workspace.spec.js @@ -23,7 +23,7 @@ const languages = [ // {language: "mustache", code: '10', checks: {numeric: '10'}}, {language: "Objective C", code: '10', checks: {numeric: '10'}}, {language: "Perl", code: '10', checks: {numeric: '10'}}, - {language: "PHP", code: '', checks: {numeric: '10'}}, + {language: "PHP", code: '', checks: {variable: '$foo'}}, // {language: "property-list", code: '10', checks: {numeric: '10'}}, {language: "Python", code: '10', checks: {numeric: '10'}}, {language: "Ruby on Rails", code: '10', checks: {numeric: '10'}}, diff --git a/packages/atom-dark-syntax/index.less b/packages/atom-dark-syntax/index.less index 161afc862..7a20f8eb9 100644 --- a/packages/atom-dark-syntax/index.less +++ b/packages/atom-dark-syntax/index.less @@ -9,3 +9,4 @@ @import "styles/syntax/base.less"; @import "styles/syntax/css.less"; @import "styles/syntax/html.less"; +@import "styles/syntax/json.less"; diff --git a/packages/atom-dark-syntax/styles/syntax/base.less b/packages/atom-dark-syntax/styles/syntax/base.less index d5fe55930..729b801df 100644 --- a/packages/atom-dark-syntax/styles/syntax/base.less +++ b/packages/atom-dark-syntax/styles/syntax/base.less @@ -229,6 +229,12 @@ &.syntax--italic { font-style: italic; } + + // Horizontal rules in GFM used to be scoped as `comment.hr`. For continuity, + // we assign the color of a comment to this new scope. + &.syntax--horizontal-rule { + color: #8A8A8A; + } } // /* comment */ diff --git a/packages/atom-dark-syntax/styles/syntax/json.less b/packages/atom-dark-syntax/styles/syntax/json.less new file mode 100644 index 000000000..011feb3b2 --- /dev/null +++ b/packages/atom-dark-syntax/styles/syntax/json.less @@ -0,0 +1,11 @@ + +.syntax--source.syntax--json { + + // Color JSON keys differently from other strings. + .syntax--meta.syntax--structure.syntax--key { + .syntax--string.syntax--quoted.syntax--double { + color: #96CBFE; + } + } + +} diff --git a/packages/atom-light-syntax/index.less b/packages/atom-light-syntax/index.less index d86a25ba2..639a66be4 100644 --- a/packages/atom-light-syntax/index.less +++ b/packages/atom-light-syntax/index.less @@ -8,3 +8,4 @@ @import "styles/syntax/base.less"; @import "styles/syntax/css.less"; +@import "styles/syntax/json.less"; diff --git a/packages/atom-light-syntax/styles/syntax/base.less b/packages/atom-light-syntax/styles/syntax/base.less index aa5de28f8..18f5ce6d8 100644 --- a/packages/atom-light-syntax/styles/syntax/base.less +++ b/packages/atom-light-syntax/styles/syntax/base.less @@ -198,6 +198,12 @@ &.syntax--italic { font-style: italic; } + + // Horizontal rules in GFM used to be scoped as `comment.hr`. For continuity, + // we assign the color of a comment to this new scope. + &.syntax--horizontal-rule { + color: #999988; + } } // /* comment */ diff --git a/packages/atom-light-syntax/styles/syntax/json.less b/packages/atom-light-syntax/styles/syntax/json.less new file mode 100644 index 000000000..6891b18a3 --- /dev/null +++ b/packages/atom-light-syntax/styles/syntax/json.less @@ -0,0 +1,11 @@ + +.syntax--source.syntax--json { + + // Color JSON keys differently from other strings. + .syntax--meta.syntax--structure.syntax--key { + .syntax--string.syntax--quoted.syntax--double { + color: #008080; + } + } + +} diff --git a/packages/base16-tomorrow-dark-theme/styles/syntax-legacy/_base.less b/packages/base16-tomorrow-dark-theme/styles/syntax-legacy/_base.less index e7eab57a6..758f42dfd 100644 --- a/packages/base16-tomorrow-dark-theme/styles/syntax-legacy/_base.less +++ b/packages/base16-tomorrow-dark-theme/styles/syntax-legacy/_base.less @@ -267,6 +267,12 @@ &.syntax--raw { color: @green; } + + // Horizontal rules in GFM used to be scoped as `comment.hr`. For continuity, + // we assign the color of a comment to this new scope. + &.syntax--horizontal-rule { + color: @gray; + } } .syntax--source.syntax--gfm { diff --git a/packages/base16-tomorrow-dark-theme/styles/syntax-legacy/json.less b/packages/base16-tomorrow-dark-theme/styles/syntax-legacy/json.less index 9d451633f..a80b366aa 100644 --- a/packages/base16-tomorrow-dark-theme/styles/syntax-legacy/json.less +++ b/packages/base16-tomorrow-dark-theme/styles/syntax-legacy/json.less @@ -8,6 +8,13 @@ } } + // Color JSON keys differently from other strings. + .syntax--meta.syntax--structure.syntax--key { + .syntax--string.syntax--quoted.syntax--double { + color: @red; + } + } + .syntax--meta.syntax--structure.syntax--dictionary.syntax--json, .syntax--meta.syntax--structure.syntax--array.syntax--json { & > .syntax--value.syntax--json > .syntax--string.syntax--quoted.syntax--json, & > .syntax--value.syntax--json > .syntax--string.syntax--quoted.syntax--json > .syntax--punctuation { diff --git a/packages/base16-tomorrow-light-theme/styles/syntax-legacy/_base.less b/packages/base16-tomorrow-light-theme/styles/syntax-legacy/_base.less index e7eab57a6..758f42dfd 100644 --- a/packages/base16-tomorrow-light-theme/styles/syntax-legacy/_base.less +++ b/packages/base16-tomorrow-light-theme/styles/syntax-legacy/_base.less @@ -267,6 +267,12 @@ &.syntax--raw { color: @green; } + + // Horizontal rules in GFM used to be scoped as `comment.hr`. For continuity, + // we assign the color of a comment to this new scope. + &.syntax--horizontal-rule { + color: @gray; + } } .syntax--source.syntax--gfm { diff --git a/packages/base16-tomorrow-light-theme/styles/syntax-legacy/json.less b/packages/base16-tomorrow-light-theme/styles/syntax-legacy/json.less index 9d451633f..a80b366aa 100644 --- a/packages/base16-tomorrow-light-theme/styles/syntax-legacy/json.less +++ b/packages/base16-tomorrow-light-theme/styles/syntax-legacy/json.less @@ -8,6 +8,13 @@ } } + // Color JSON keys differently from other strings. + .syntax--meta.syntax--structure.syntax--key { + .syntax--string.syntax--quoted.syntax--double { + color: @red; + } + } + .syntax--meta.syntax--structure.syntax--dictionary.syntax--json, .syntax--meta.syntax--structure.syntax--array.syntax--json { & > .syntax--value.syntax--json > .syntax--string.syntax--quoted.syntax--json, & > .syntax--value.syntax--json > .syntax--string.syntax--quoted.syntax--json > .syntax--punctuation { diff --git a/packages/bracket-matcher/spec/.eslintrc.js b/packages/bracket-matcher/spec/.eslintrc.js new file mode 100644 index 000000000..fa2293002 --- /dev/null +++ b/packages/bracket-matcher/spec/.eslintrc.js @@ -0,0 +1,12 @@ +module.exports = { + env: { jasmine: true }, + globals: { + waitsForPromise: true, + }, + rules: { + "node/no-unpublished-require": "off", + "node/no-extraneous-require": "off", + "no-unused-vars": "off", + "no-empty": "off" + } +}; diff --git a/packages/language-c/grammars/modern-tree-sitter-c.cson b/packages/language-c/grammars/modern-tree-sitter-c.cson index c1f376e33..c4be00a85 100644 --- a/packages/language-c/grammars/modern-tree-sitter-c.cson +++ b/packages/language-c/grammars/modern-tree-sitter-c.cson @@ -7,6 +7,7 @@ firstLineRegex: '-\\*-[^*]*(Mode:\\s*)?C(\\s*;.*?)?\\s*-\\*-' injectionRegex: '^(c|C)$' treeSitter: + parserSource: 'github:tree-sitter/tree-sitter-c#212a80f86452bb1316324fa0db730cf52f29e05a' grammar: 'tree-sitter-c/tree-sitter-c.wasm' highlightsQuery: 'tree-sitter-c/highlights.scm' tagsQuery: 'tree-sitter-c/tags.scm' diff --git a/packages/language-c/grammars/modern-tree-sitter-cpp.cson b/packages/language-c/grammars/modern-tree-sitter-cpp.cson index 98352d101..93e1c2978 100644 --- a/packages/language-c/grammars/modern-tree-sitter-cpp.cson +++ b/packages/language-c/grammars/modern-tree-sitter-cpp.cson @@ -6,7 +6,7 @@ parser: 'tree-sitter-cpp' injectionRegex: '^(c|C)(\\+\\+|pp|PP)$' treeSitter: - parserSource: 'github:tree-sitter/tree-sitter-cpp#a90f170f92d5d70e7c2d4183c146e61ba5f3a457' + parserSource: 'github:tree-sitter/tree-sitter-cpp#a71474021410973b29bfe99440d57bcd750246b1' grammar: 'tree-sitter-cpp/tree-sitter-cpp.wasm' highlightsQuery: 'tree-sitter-cpp/highlights.scm' tagsQuery: 'tree-sitter-cpp/tags.scm' diff --git a/packages/language-c/grammars/tree-sitter-c/highlights.scm b/packages/language-c/grammars/tree-sitter-c/highlights.scm index 96dccf2de..3a2db6c79 100644 --- a/packages/language-c/grammars/tree-sitter-c/highlights.scm +++ b/packages/language-c/grammars/tree-sitter-c/highlights.scm @@ -13,6 +13,10 @@ "#define" @keyword.control.directive.define.c "#include" @keyword.control.directive.include.c +(["#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. @@ -43,30 +47,41 @@ (type_identifier) @_IGNORE_ (#set! capture.final true)) -(primitive_type) @support.type.builtin.c -(type_identifier) @support.type.other.c +(primitive_type) @support.storage.type.builtin.c +(type_identifier) @support.other.storage.type.c ; These types are all reserved words; if we see an identifier with this name, ; it must be a type. -((identifier) @support.type.builtin.c - (#match? @support.type.builtin.c "^(char|int|float|double|long)$")) +((identifier) @support.storage.type.builtin.c + (#match? @support.storage.type.builtin.c "^(char|int|float|double|long)$")) ; Assume any identifier that ends in `_t` is a type. This convention is not ; always followed, but it's a very strong indicator when it's present. -((identifier) @support.type.other.c - (#match? @support.type.other.c "_t$")) +((identifier) @support.other.storage.type.c + (#match? @support.other.storage.type.c "_t$")) +; These refer to language constructs and remain in the `storage` namespace. [ "enum" - "long" - "short" - "signed" "struct" "typedef" "union" - "unsigned" ] @storage.type.c +; These refer to value types and go under `support`. +[ + "long" + "short" +] @support.storage.type.builtin.c + +; These act as modifiers to value types and also go under `support`. +[ + "signed" + "unsigned" +] @support.storage.modifier.builtin.c + +; These act as general language modifiers and remain in the `storage` +; namespace. [ "const" "extern" @@ -75,10 +90,10 @@ "restrict" "static" "volatile" -] @storage.modifier.c +] @storage.modifier._TYPE_.c -((primitive_type) @support.type.stdint.c - (#match? @support.type.stdint.c "^(int8_t|int16_t|int32_t|int64_t|uint8_t|uint16_t|uint32_t|uint64_t|int_least8_t|int_least16_t|int_least32_t|int_least64_t|uint_least8_t|uint_least16_t|uint_least32_t|uint_least64_t|int_fast8_t|int_fast16_t|int_fast32_t|int_fast64_t|uint_fast8_t|uint_fast16_t|uint_fast32_t|uint_fast64_t|intptr_t|uintptr_t|intmax_t|intmax_t|uintmax_t|uintmax_t)$")) +((primitive_type) @support.storage.type.stdint.c + (#match? @support.storage.type.stdint.c "^(int8_t|int16_t|int32_t|int64_t|uint8_t|uint16_t|uint32_t|uint64_t|int_least8_t|int_least16_t|int_least32_t|int_least64_t|uint_least8_t|uint_least16_t|uint_least32_t|uint_least64_t|int_fast8_t|int_fast16_t|int_fast32_t|int_fast64_t|uint_fast8_t|uint_fast16_t|uint_fast32_t|uint_fast64_t|intptr_t|uintptr_t|intmax_t|intmax_t|uintmax_t|uintmax_t)$")) (enum_specifier name: (type_identifier) @variable.other.declaration.type.c) @@ -116,32 +131,57 @@ ; Declarations and assignments ; ---------------------------- -; The "x" in `int x`; +; The "x" in `int x;` (declaration declarator: (identifier) @variable.declaration.c) -; The "x" in `int x = y`; +; The "x" in `int x = y;` (init_declarator declarator: (identifier) @variable.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 + (#is? test.descendantOfType "declaration field_declaration")) + +; A member of a struct. (field_declaration - (field_identifier) @entity.other.attribute-name.c) + (field_identifier) @variable.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)) + +; (and the associated ".") +(initializer_pair + (field_designator + "." @keyword.operator.accessor.c)) (field_declaration (pointer_declarator - (field_identifier) @entity.other.attribute-name.c)) + (field_identifier) @variable.declaration.member.c)) (field_declaration (array_declarator - (field_identifier) @entity.other.attribute-name.c)) + (field_identifier) @variable.declaration.member.c)) (init_declarator (pointer_declarator - (identifier) @entity.other.attribute-name.c)) + (identifier) @variable.declaration.member.c)) +; The "x" in `x = y;` (assignment_expression left: (identifier) @variable.other.assignment.c) +; The "foo" in `something->foo = "bar";` +(assignment_expression + left: (field_expression + field: (field_identifier) @variable.other.member.assignment.c) + (#set! capture.final)) + ; Function parameters ; ------------------- @@ -154,9 +194,10 @@ declarator: (identifier) @variable.parameter.c) ; The "foo" in `const char *foo` within a parameter list. -(parameter_declaration - declarator: (pointer_declarator - declarator: (identifier) @variable.parameter.c)) +; (Should work no matter how many pointers deep we are.) +(pointer_declarator + declarator: [(identifier) (field_identifier)] @variable.parameter.pointer.c + (#is? test.descendantOfType "parameter_declaration")) ; The "foo" in `const char foo[]` within a parameter list. (parameter_declaration @@ -172,7 +213,7 @@ ; The "size" in `finfo->size`. (field_expression "->" - field: (field_identifier) @support.other.property.c) + field: (field_identifier) @variable.other.member.c) ; FUNCTIONS @@ -309,8 +350,10 @@ ";" @punctuation.terminator.statement.c -"," @punctuation.separator.comma.c -"->" @punctuation.separator.pointer-access.c +("," @punctuation.separator.comma.c + (#set! capture.shy)) +("->" @keyword.operator.accessor.pointer-access.c + (#set! capture.shy)) (parameter_list "(" @punctuation.definition.parameters.begin.bracket.round.c @@ -335,6 +378,22 @@ "[" @punctuation.definition.array.begin.bracket.square.c "]" @punctuation.definition.array.end.bracket.square.c + +; META +; ==== + +((compound_statement) @meta.block.c + (#set! adjust.startAt firstChild.endPosition) + (#set! adjust.endAt lastChild.startPosition)) + +((enumerator_list) @meta.block.enum.c + (#set! adjust.startAt firstChild.endPosition) + (#set! adjust.endAt lastChild.startPosition)) + +((field_declaration_list) @meta.block.field.c + (#set! adjust.startAt firstChild.endPosition) + (#set! adjust.endAt lastChild.startPosition)) + ; TODO: ; ; * TM-style grammar has a lot of `mac-classic` scopes. I doubt they'd be diff --git a/packages/language-c/grammars/tree-sitter-c/tree-sitter-c.wasm b/packages/language-c/grammars/tree-sitter-c/tree-sitter-c.wasm index 3f54a976c..771b9c53b 100755 Binary files a/packages/language-c/grammars/tree-sitter-c/tree-sitter-c.wasm and b/packages/language-c/grammars/tree-sitter-c/tree-sitter-c.wasm differ diff --git a/packages/language-c/grammars/tree-sitter-cpp/highlights.scm b/packages/language-c/grammars/tree-sitter-cpp/highlights.scm index 335640633..bc3fc2c3c 100644 --- a/packages/language-c/grammars/tree-sitter-cpp/highlights.scm +++ b/packages/language-c/grammars/tree-sitter-cpp/highlights.scm @@ -13,6 +13,10 @@ "#define" @keyword.control.directive.define.cpp "#include" @keyword.control.directive.include.cpp +(["#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. @@ -69,28 +73,38 @@ ; These types are all reserved words; if we see an identifier with this name, ; it must be a type. -((identifier) @support.type.builtin.cpp - (#match? @support.type.builtin.cpp "^(char|int|float|double|long)$")) +((identifier) @support.storage.type.builtin.cpp + (#match? @support.storage.type.builtin.cpp "^(char|int|float|double|long)$")) ; Assume any identifier that ends in `_t` is a type. This convention is not ; always followed, but it's a very strong indicator when it's present. -((identifier) @support.type.other.cpp - (#match? @support.type.other.cpp "_t$")) +((identifier) @support.other.storage.type.cpp + (#match? @support.other.storage.type.cpp "_t$")) +; These refer to language constructs and remain in the `storage` namespace. [ "enum" - "long" - "short" - "signed" "struct" "typedef" "union" - "unsigned" - "template" ] @storage.type.cpp +; These refer to value types and go under `support`. +[ + "long" + "short" +] @support.storage.type.builtin.cpp + +; These act as modifiers to value types and also go under `support`. +[ + "signed" + "unsigned" +] @support.storage.modifier.builtin.cpp + +; These act as general language modifiers and remain in the `storage` +; namespace. [ "const" "extern" @@ -110,15 +124,15 @@ "override" "final" "noexcept" -] @storage.modifier.cpp + + "typename" +] @storage.modifier._TYPE_.cpp ( - (primitive_type) @support.type.stdint.cpp - (#match? @support.type.stdint.cpp "^(int8_t|int16_t|int32_t|int64_t|uint8_t|uint16_t|uint32_t|uint64_t|int_least8_t|int_least16_t|int_least32_t|int_least64_t|uint_least8_t|uint_least16_t|uint_least32_t|uint_least64_t|int_fast8_t|int_fast16_t|int_fast32_t|int_fast64_t|uint_fast8_t|uint_fast16_t|uint_fast32_t|uint_fast64_t|intptr_t|uintptr_t|intmax_t|intmax_t|uintmax_t|uintmax_t)$") + (primitive_type) @support.storage.type.stdint.cpp + (#match? @support.storage.type.stdint.cpp "^(int8_t|int16_t|int32_t|int64_t|uint8_t|uint16_t|uint32_t|uint64_t|int_least8_t|int_least16_t|int_least32_t|int_least64_t|uint_least8_t|uint_least16_t|uint_least32_t|uint_least64_t|int_fast8_t|int_fast16_t|int_fast32_t|int_fast64_t|uint_fast8_t|uint_fast16_t|uint_fast32_t|uint_fast64_t|intptr_t|uintptr_t|intmax_t|intmax_t|uintmax_t|uintmax_t)$") ) -"typename" @storage.modifier.typename.cpp - ; FUNCTIONS ; ========= @@ -207,36 +221,56 @@ ; Declarations and assignments ; ---------------------------- -; The "x" in `int x`; +; The "x" in `int x;` (declaration declarator: (identifier) @variable.declaration.cpp) -; The "x" in `int x = y`; +; The "x" in `int x = y;` (init_declarator declarator: (identifier) @variable.declaration.cpp) +; 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 + (#is? test.descendantOfType "declaration field_declaration")) + +; A member of a struct. (field_declaration - (field_identifier) @variable.declaration.cpp) + (field_identifier) @variable.declaration.member.cpp) + +; 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.cpp)) + +; (and the associated ".") +(initializer_pair + (field_designator + "." @keyword.operator.accessor.cpp)) (field_declaration (pointer_declarator - (field_identifier) @variable.declaration.cpp)) + (field_identifier) @variable.declaration.member.cpp)) (field_declaration (array_declarator - (field_identifier) @variable.declaration.cpp)) + (field_identifier) @variable.declaration.member.cpp)) (init_declarator (pointer_declarator - (identifier) @variable.declaration.cpp)) + (identifier) @variable.declaration.member.cpp)) +; The "x" in `x = y;` (assignment_expression left: (identifier) @variable.other.assignment.cpp) -; The "foo" in `bar.foo = "baz"`. +; The "foo" in `something->foo = "bar";` (assignment_expression left: (field_expression - field: (field_identifier) @variable.other.member.assignment.cpp)) + field: (field_identifier) @variable.other.member.assignment.cpp) + (#set! capture.final)) ((reference_declarator (identifier) @variable.declaration.cpp) @@ -248,18 +282,20 @@ (preproc_params (identifier) @variable.parameter.preprocessor.cpp) +; The "foo" in `const char foo` within a parameter list. (parameter_declaration declarator: (identifier) @variable.parameter.cpp) -(parameter_declaration - declarator: (pointer_declarator - declarator: (identifier) @variable.parameter.cpp)) +; 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 + (#is? test.descendantOfType "parameter_declaration")) (parameter_declaration declarator: (reference_declarator (identifier) @variable.parameter.cpp)) - ; The "foo" in `const char foo[]` within a parameter list. (parameter_declaration declarator: (array_declarator @@ -420,8 +456,10 @@ ";" @punctuation.terminator.statement.cpp -"," @punctuation.separator.comma.cpp -"->" @keyword.operator.accessor.cpp +("," @punctuation.separator.comma.cpp + (#set! capture.shy)) +("->" @keyword.operator.accessor.pointer-access.cpp + (#set! capture.shy)) (parameter_list "(" @punctuation.definition.parameters.begin.bracket.round.cpp @@ -446,6 +484,22 @@ "[" @punctuation.definition.array.begin.bracket.square.cpp "]" @punctuation.definition.array.end.bracket.square.cpp +; META +; ==== + +((compound_statement) @meta.block.cpp + (#set! adjust.startAt firstChild.endPosition) + (#set! adjust.endAt lastChild.startPosition)) + +((enumerator_list) @meta.block.enum.cpp + (#set! adjust.startAt firstChild.endPosition) + (#set! adjust.endAt lastChild.startPosition)) + +((field_declaration_list) @meta.block.field.cpp + (#set! adjust.startAt firstChild.endPosition) + (#set! adjust.endAt lastChild.startPosition)) + + ; TODO: ; ; * TM-style grammar has a lot of `mac-classic` scopes. I doubt they'd be diff --git a/packages/language-c/grammars/tree-sitter-cpp/tree-sitter-cpp.wasm b/packages/language-c/grammars/tree-sitter-cpp/tree-sitter-cpp.wasm index ae93e20ae..002712b10 100755 Binary files a/packages/language-c/grammars/tree-sitter-cpp/tree-sitter-cpp.wasm and b/packages/language-c/grammars/tree-sitter-cpp/tree-sitter-cpp.wasm differ diff --git a/packages/language-c/lib/main.js b/packages/language-c/lib/main.js index 6e432cd64..1ae428d1d 100644 --- a/packages/language-c/lib/main.js +++ b/packages/language-c/lib/main.js @@ -12,28 +12,19 @@ exports.activate = function () { } }); } - - const TODO_PATTERN = /\b(TODO|FIXME|CHANGED|XXX|IDEA|HACK|NOTE|REVIEW|NB|BUG|QUESTION|COMBAK|TEMP|DEBUG|OPTIMIZE|WARNING)\b/; - const HYPERLINK_PATTERN = /\bhttps?:/ - - atom.grammars.addInjectionPoint(`source.${language}`, { - type: 'comment', - language: (node) => { - return TODO_PATTERN.test(node.text) ? 'todo' : undefined; - }, - content: (node) => node, - languageScope: null - }); - - for (let type of ['string_literal', 'comment']) { - atom.grammars.addInjectionPoint(`source.${language}`, { - type, - language: (node) => { - return HYPERLINK_PATTERN.test(node.text) ? 'hyperlink' : undefined; - }, - content: (node) => node, - languageScope: null - }); - } + } +}; + +exports.consumeHyperlinkInjection = (hyperlink) => { + for (const language of ['c', 'cpp']) { + hyperlink.addInjectionPoint(`source.${language}`, { + types: ['comment', 'string_literal'] + }); + } +}; + +exports.consumeTodoInjection = (todo) => { + for (const language of ['c', 'cpp']) { + todo.addInjectionPoint(`source.${language}`, { types: ['comment'] }); } }; diff --git a/packages/language-c/package.json b/packages/language-c/package.json index 2b891a171..845d28471 100644 --- a/packages/language-c/package.json +++ b/packages/language-c/package.json @@ -15,5 +15,17 @@ "dependencies": { "tree-sitter-c": "0.20.2", "tree-sitter-cpp": "0.20.0" + }, + "consumedServices": { + "hyperlink.injection": { + "versions": { + "0.1.0": "consumeHyperlinkInjection" + } + }, + "todo.injection": { + "versions": { + "0.1.0": "consumeTodoInjection" + } + } } } diff --git a/packages/language-css/grammars/modern-tree-sitter-css.cson b/packages/language-css/grammars/modern-tree-sitter-css.cson index 87069bcd3..2c0f5ff50 100644 --- a/packages/language-css/grammars/modern-tree-sitter-css.cson +++ b/packages/language-css/grammars/modern-tree-sitter-css.cson @@ -8,6 +8,7 @@ fileTypes: [ ] treeSitter: + parserSource: 'github:tree-sitter/tree-sitter-css#98c7b3dceb24f1ee17f1322f3947e55638251c37' grammar: 'tree-sitter/tree-sitter-css.wasm' highlightsQuery: 'tree-sitter/queries/highlights.scm' foldsQuery: 'tree-sitter/queries/folds.scm' diff --git a/packages/language-css/grammars/tree-sitter/queries/highlights.scm b/packages/language-css/grammars/tree-sitter/queries/highlights.scm index 685e46243..4caea0895 100644 --- a/packages/language-css/grammars/tree-sitter/queries/highlights.scm +++ b/packages/language-css/grammars/tree-sitter/queries/highlights.scm @@ -1,36 +1,36 @@ -; WORKAROUND: + +; NOTE: `tree-sitter-css` recovers poorly from invalidity inside a block when +; you're adding a new property-value pair above others in a list. When the user +; is typing and the file is temporarily invalid, it will make incorrect guesses +; about tokens that occur between the cursor and the end of the block. ; -; When you're typing a new property name inside of a list, tree-sitter-css will -; assume the thing you're typing is a descendant selector tag name until you -; get to the colon. This prevents it from highlighting the incomplete line like -; a selector tag name. +; The fix here is for `tree-sitter-css` to get better at recovering from its +; parsing error, but parser authors don't currently have much control over +; that. In the meantime, this query is a decent mitigation: it colors the +; affected tokens like plain text instead of assuming (nearly always +; incorrectly) them to be tag names. +; +; Ideally, this is temporary, and we can remove it soon. Until then, it makes +; syntax highlighting less obnoxious. -(descendant_selector - (tag_name) @_IGNORE_ - (#set! capture.final true)) +((tag_name) @_IGNORE_ + (#is? test.descendantOfType "ERROR") + (#set! capture.final)) (ERROR (attribute_name) @_IGNORE_ - (#set! capture.final true)) + (#set! capture.final)) ((ERROR (attribute_name) @invalid.illegal) - (#set! capture.final true)) + (#set! capture.final)) ; WORKAROUND: ; -; `:hover` and other pseudo-classes don't highlight correctly inside a media -; query (https://github.com/tree-sitter/tree-sitter-css/issues/28) -( - (ERROR) @entity.other.attribute-name.pseudo-class.css - (#match? @entity.other.attribute-name.pseudo-class.css "^:[\\w-]+$") -) - -; WORKAROUND: -; -; In `::after`, the "after" has a node type of `tag_name`. We want to catch it -; here so that it doesn't get scoped like an HTML tag name in a selector. +; In `::after`, the "after" has a node type of `tag_name`. Unclear whether this +; is a bug or intended behavior. We want to catch it here so that it doesn't +; get scoped like an HTML tag name in a selector. ; Scope the entire `::after` range as one unit. ((pseudo_element_selector) @@ -61,9 +61,6 @@ ; (selectors "," @punctuation.separator.list.comma.css) -; The "div" in `div.foo {`. -(tag_name) @entity.name.tag.css - ; The "foo" in `div[attr=foo] {`. (attribute_selector (plain_value) @string.unquoted.css) @@ -77,10 +74,30 @@ (id_selector "#" @punctuation.definition.entity.id.css) @entity.other.attribute-name.id.css -; KNOWN ISSUE: Namespace selectors like `svg|link` are not supported. See: -; https://github.com/tree-sitter/tree-sitter-css/issues/33 +; Declaration of a namespace: +; The "svg" in `@namespace svg url(http://www.w3.org/2000/svg);` +(namespace_name) @entity.other.namespace-prefix.css -;(namespace_name) @entity.other.namespace-prefix.css +; A namespaced tag name: +; The "svg" in `svg|a {}`. +(namespace_selector + . (tag_name) @entity.other.namespace-prefix.css + "|" @punctuation.separator.namespace.css + (#set! capture.final)) + +; Not sure if this is intended, but a namespaced attribute in an attribute +; selector is construed as two tag-name children of the `attribute_name`. +; The "xl" in `[xl|href] {}`. +(attribute_name + . (tag_name) @entity.other.namespace-prefix.css + "|" @punctuation.separator.namespace.css + (tag_name) @entity.other.attribute_name.css + (#set! capture.final)) @_IGNORE_ + +; The "div" in `div.foo {`. +(tag_name) @entity.name.tag.css +; The "*" in `*[foo="bar"]`. +(universal_selector) @entity.name.tag.universal.css ; The '.' in `.foo`. (class_selector @@ -101,29 +118,41 @@ (#set! adjust.startAt lastChild.previousSibling.startPosition) (#set! adjust.endAt lastChild.endPosition)) +; Punctuation around the arguments of a pseudo-class or a function. (arguments "(" @punctuation.definition.arguments.begin.bracket.round.css ")" @punctuation.definition.arguments.end.bracket.round.css) +; Punctuation around an attribute selector. (attribute_selector "[" @punctuation.definition.entity.begin.bracket.square.css (attribute_name) @entity.other.attribute-name.css "]" @punctuation.definition.entity.end.bracket.square.css) +; Operators inside attribute selectors. (attribute_selector ["=" "^=" "$=" "~=" "|="] @keyword.operator.pattern.css) -; The `foo` in `@keyframes foo {`. +; The "foo" in `@keyframes foo {`. (keyframes_name) @entity.name.keyframes.css ; VARIABLES ; ========= +; Variable declaration: +; The "--link-visited" in `--link-visited: #039;`. (declaration (property_name) @variable.other.assignment.css (#match? @variable.other.assignment.css "^--" ) (#set! capture.final true)) +; Variable usage: +; The ""--link--visited" in `color: var(--link-visited);`. +((function_name) @support.function.var.css + (arguments (plain_value) @variable.css) + (#eq? @support.function.var.css "var")) + + ; PROPERTIES ; ========== @@ -147,9 +176,9 @@ (#match? @string.quoted.single.css "^'") (#match? @string.quoted.single.css "'$")) +; The punctuation around quoted strings. ((string_value) @punctuation.definition.string.begin.css (#set! adjust.startAndEndAroundFirstMatchOf "^[\"']")) - ((string_value) @punctuation.definition.string.end.css (#set! adjust.startAndEndAroundFirstMatchOf "[\"']$")) @@ -208,10 +237,6 @@ ; (#eq? @support.function.var.css "var") ; ) -((function_name) @support.function.var.css - (arguments (plain_value) @variable.css) - (#eq? @support.function.var.css "var")) - ((function_name) @support.function._TEXT_.css ; Because we just handled it above. (#not-eq? @support.function._TEXT_.css "var")) diff --git a/packages/language-css/grammars/tree-sitter/tree-sitter-css.wasm b/packages/language-css/grammars/tree-sitter/tree-sitter-css.wasm index f6293088e..711c7732f 100755 Binary files a/packages/language-css/grammars/tree-sitter/tree-sitter-css.wasm and b/packages/language-css/grammars/tree-sitter/tree-sitter-css.wasm differ diff --git a/packages/language-css/lib/main.js b/packages/language-css/lib/main.js index c12fb4b56..cf2d7837c 100644 --- a/packages/language-css/lib/main.js +++ b/packages/language-css/lib/main.js @@ -1,43 +1,26 @@ -exports.activate = () => { - const TODO_PATTERN = /\b(TODO|FIXME|CHANGED|XXX|IDEA|HACK|NOTE|REVIEW|NB|BUG|QUESTION|COMBAK|TEMP|DEBUG|OPTIMIZE|WARNING)\b/; - const HYPERLINK_PATTERN = /\bhttps?:/ - - atom.grammars.addInjectionPoint('source.css', { - type: 'comment', - language(node) { - return TODO_PATTERN.test(node.text) ? 'todo' : undefined; - }, - content: (node) => node, - languageScope: null +exports.consumeHyperlinkInjection = (hyperlink) => { + hyperlink.addInjectionPoint('source.css', { + types: ['comment', 'string_value'] }); - for (let type of ['comment', 'string_value']) { - atom.grammars.addInjectionPoint('source.css', { - type, - language(node) { - return HYPERLINK_PATTERN.test(node.text) ? 'hyperlink' : undefined; - }, - content: (node) => node, - languageScope: null - }); - } - // Catch things like // // @import url(https://www.example.com/style.css); // // where the URL is unquoted. - atom.grammars.addInjectionPoint('source.css', { - type: 'call_expression', + hyperlink.addInjectionPoint('source.css', { + types: ['call_expression'], language: () => 'hyperlink', - content: (node) => { + content(node) { let functionName = node.descendantsOfType('function_value')[0]?.text; if (!functionName === 'url') { return null; } return node.descendantsOfType('plain_value'); - }, - languageScope: null + } }); - +}; + +exports.consumeTodoInjection = (todo) => { + todo.addInjectionPoint('source.css', { types: ['comment'] }); }; diff --git a/packages/language-css/package.json b/packages/language-css/package.json index 96c2d0d7a..7e2d03d1b 100644 --- a/packages/language-css/package.json +++ b/packages/language-css/package.json @@ -14,5 +14,17 @@ "license": "MIT", "dependencies": { "tree-sitter-css": "^0.19.0" + }, + "consumedServices": { + "hyperlink.injection": { + "versions": { + "0.1.0": "consumeHyperlinkInjection" + } + }, + "todo.injection": { + "versions": { + "0.1.0": "consumeTodoInjection" + } + } } } diff --git a/packages/language-gfm/grammars/tree-sitter/tree-sitter-markdown/highlights.scm b/packages/language-gfm/grammars/tree-sitter/tree-sitter-markdown/highlights.scm index 220a409c0..42e12ebe6 100644 --- a/packages/language-gfm/grammars/tree-sitter/tree-sitter-markdown/highlights.scm +++ b/packages/language-gfm/grammars/tree-sitter/tree-sitter-markdown/highlights.scm @@ -39,7 +39,7 @@ (paragraph) @markup.paragraph.gfm -(thematic_break) @punctuation.definition.horizontal-rule.gfm +(thematic_break) @markup.horizontal-rule.gfm (block_quote) @markup.quote.blockquote.gfm ((block_quote) @punctuation.definition.blockquote.gfm @@ -140,8 +140,9 @@ (code_span) @meta.embedded.line.inline-code.gfm @markup.raw.inline.gfm (info_string) @storage.modifier.language._TEXT_.gfm -(fenced_code_block) @markup.code.fenced.gfm @meta.embedded.block.fenced-code.gfm -(indented_code_block) @markup.code.indented.gfm @meta.embedded.block.indented-code.gfm +(fenced_code_block + (code_fence_content) @markup.raw.block.fenced.gfm) @meta.embedded.block.fenced-code.gfm +(indented_code_block) @markup.raw.block.indented.gfm @meta.embedded.block.indented-code.gfm ; BOLD/ITALIC/OTHER diff --git a/packages/language-gfm/lib/main.js b/packages/language-gfm/lib/main.js index f416bb04a..d4c62e1b4 100644 --- a/packages/language-gfm/lib/main.js +++ b/packages/language-gfm/lib/main.js @@ -88,3 +88,40 @@ exports.activate = () => { includeChildren: true }); }; + + +// Since this parser isn't guaranteed to detect all URLs in paragraphs (see +// https://github.com/pulsar-edit/pulsar/issues/885), we'll inject the +// `hyperlink` parser into `text` nodes in paragraphs when there appear to be +// URLs in them. +exports.consumeHyperlinkInjection = (hyperlink) => { + + function textChildren(node) { + let results = []; + for (let i = 0; i < node.namedChildCount; i++) { + let child = node.child(i); + if (child.type === 'text') { + results.push(child); + } + } + return results; + } + + hyperlink.addInjectionPoint('source.gfm.embedded', { + types: ['paragraph'], + // Override the language callback so that it doesn't test URLs that are + // already handled in `uri_autolink` nodes. + language(node) { + for (let child of textChildren(node)) { + if (hyperlink.test(child)) { + return 'hyperlink'; + } + } + return null; + }, + content(node) { + return textChildren(node); + } + }); + +}; diff --git a/packages/language-gfm/package.json b/packages/language-gfm/package.json index 54610b63b..f3d297ea7 100644 --- a/packages/language-gfm/package.json +++ b/packages/language-gfm/package.json @@ -10,5 +10,12 @@ }, "devDependencies": { "coffeescript": "^1.7.0" - } + }, + "consumedServices": { + "hyperlink.injection": { + "versions": { + "0.1.0": "consumeHyperlinkInjection" + } + } + } } diff --git a/packages/language-go/grammars/modern-tree-sitter-go.cson b/packages/language-go/grammars/modern-tree-sitter-go.cson index 3f9166c42..5a0eab7e4 100644 --- a/packages/language-go/grammars/modern-tree-sitter-go.cson +++ b/packages/language-go/grammars/modern-tree-sitter-go.cson @@ -11,6 +11,7 @@ comments: start: '// ' treeSitter: + parserSource: 'github:tree-sitter/tree-sitter-go#ff86c7f1734873c8c4874ca4dd95603695686d7a' grammar: 'tree-sitter-go/tree-sitter-go.wasm' highlightsQuery: 'tree-sitter-go/highlights.scm' foldsQuery: 'tree-sitter-go/folds.scm' diff --git a/packages/language-go/grammars/tree-sitter-go/highlights.scm b/packages/language-go/grammars/tree-sitter-go/highlights.scm index b3bc38a65..330d4455a 100644 --- a/packages/language-go/grammars/tree-sitter-go/highlights.scm +++ b/packages/language-go/grammars/tree-sitter-go/highlights.scm @@ -47,6 +47,8 @@ [ "struct" + "interface" + "map" ] @storage.type._TYPE_.go (struct_type @@ -55,13 +57,14 @@ (field_identifier) @entity.other.attribute-name.go))) (keyed_element - (field_identifier) @entity.other.attribute-name.go - . - ":" @punctuation.separator.key-value.go) + . (literal_element) @entity.other.attribute-name.go) + +(keyed_element ":" @punctuation.separator.key-value.go) [ "break" "case" + "chan" "continue" "default" "defer" @@ -78,7 +81,10 @@ ] @keyword.control._TYPE_.go +; Function names: the "foo" in `func foo() {` (function_declaration (identifier) @entity.name.function.go) +; Method names: the "Foo" in `func (x Bar) Foo {` +(method_declaration (field_identifier) @entity.name.function.method.go) (call_expression (identifier) @support.function.builtin.go @@ -252,7 +258,8 @@ ";" @punctuation.terminator.go "," @punctuation.separator.comma.go -":" @punctuation.separator.colon.go +(":" @punctuation.separator.colon.go + (#set! capture.shy)) (parameter_list "(" @punctuation.definition.parameters.begin.bracket.round.go diff --git a/packages/language-go/grammars/tree-sitter-go/tree-sitter-go.wasm b/packages/language-go/grammars/tree-sitter-go/tree-sitter-go.wasm index a748e9e1d..1d17adba7 100755 Binary files a/packages/language-go/grammars/tree-sitter-go/tree-sitter-go.wasm and b/packages/language-go/grammars/tree-sitter-go/tree-sitter-go.wasm differ diff --git a/packages/language-go/lib/main.js b/packages/language-go/lib/main.js index a74d32bef..50a8e11ff 100644 --- a/packages/language-go/lib/main.js +++ b/packages/language-go/lib/main.js @@ -1,25 +1,10 @@ -exports.activate = () => { - const TODO_PATTERN = /\b(TODO|FIXME|CHANGED|XXX|IDEA|HACK|NOTE|REVIEW|NB|BUG|QUESTION|COMBAK|TEMP|DEBUG|OPTIMIZE|WARNING)\b/; - const HYPERLINK_PATTERN = /\bhttps?:/ - for (let type of ['comment', 'interpreted_string_literal', 'raw_string_literal']) { - atom.grammars.addInjectionPoint('source.go', { - type, - language: (node) => { - return HYPERLINK_PATTERN.test(node.text) ? 'hyperlink' : undefined; - }, - content: (node) => node, - languageScope: null - }); - } - - atom.grammars.addInjectionPoint('source.go', { - type: 'comment', - language(node) { - return TODO_PATTERN.test(node.text) ? 'todo' : undefined; - }, - content: (node) => node, - languageScope: null +exports.consumeHyperlinkInjection = (hyperlink) => { + hyperlink.addInjectionPoint('source.go', { + types: ['comment', 'interpreted_string_literal', 'raw_string_literal'] }); - +}; + +exports.consumeTodoInjection = (todo) => { + todo.addInjectionPoint('source.go', { types: ['comment'] }); }; diff --git a/packages/language-go/package.json b/packages/language-go/package.json index 91bb8d0e0..01a1355f6 100644 --- a/packages/language-go/package.json +++ b/packages/language-go/package.json @@ -14,5 +14,17 @@ "repository": "https://github.com/pulsar-edit/pulsar", "dependencies": { "tree-sitter-go": "0.19.1" + }, + "consumedServices": { + "hyperlink.injection": { + "versions": { + "0.1.0": "consumeHyperlinkInjection" + } + }, + "todo.injection": { + "versions": { + "0.1.0": "consumeTodoInjection" + } + } } } diff --git a/packages/language-html/grammars/modern-tree-sitter-ejs.cson b/packages/language-html/grammars/modern-tree-sitter-ejs.cson index f3b7bf157..4b54d8bdd 100644 --- a/packages/language-html/grammars/modern-tree-sitter-ejs.cson +++ b/packages/language-html/grammars/modern-tree-sitter-ejs.cson @@ -11,6 +11,7 @@ fileTypes: [ injectionRegex: '^(ejs|EJS)$' treeSitter: + parserSource: 'github:tree-sitter/tree-sitter-embedded-template#203f7bd3c1bbfbd98fc19add4b8fcb213c059205' grammar: 'tree-sitter-embedded-template/tree-sitter-embedded-template.wasm' highlightsQuery: 'tree-sitter-embedded-template/ejs/highlights.scm' foldsQuery: 'tree-sitter-embedded-template/ejs/folds.scm' diff --git a/packages/language-html/grammars/modern-tree-sitter-erb.cson b/packages/language-html/grammars/modern-tree-sitter-erb.cson index da4750fd9..1e7d28e6e 100644 --- a/packages/language-html/grammars/modern-tree-sitter-erb.cson +++ b/packages/language-html/grammars/modern-tree-sitter-erb.cson @@ -11,6 +11,7 @@ fileTypes: [ injectionRegex: '^(erb|ERB)$' treeSitter: + parserSource: 'github:tree-sitter/tree-sitter-embedded-template#203f7bd3c1bbfbd98fc19add4b8fcb213c059205' grammar: 'tree-sitter-embedded-template/tree-sitter-embedded-template.wasm' highlightsQuery: 'tree-sitter-embedded-template/erb/highlights.scm' foldsQuery: 'tree-sitter-embedded-template/erb/folds.scm' diff --git a/packages/language-html/grammars/modern-tree-sitter-html.cson b/packages/language-html/grammars/modern-tree-sitter-html.cson index 5cdc42ba0..fa5fcc41c 100644 --- a/packages/language-html/grammars/modern-tree-sitter-html.cson +++ b/packages/language-html/grammars/modern-tree-sitter-html.cson @@ -6,6 +6,7 @@ parser: 'tree-sitter-html' injectionRegex: '(HTML|html|Html)$' treeSitter: + parserSource: 'github:tree-sitter/tree-sitter-html#d742025fa2d8e6100f134a6ea990443aa1f074b3' grammar: 'tree-sitter-html/tree-sitter-html.wasm' highlightsQuery: 'tree-sitter-html/highlights.scm' foldsQuery: 'tree-sitter-html/folds.scm' diff --git a/packages/language-html/grammars/tree-sitter-embedded-template/tree-sitter-embedded-template.wasm b/packages/language-html/grammars/tree-sitter-embedded-template/tree-sitter-embedded-template.wasm index 756f894af..eec102a46 100755 Binary files a/packages/language-html/grammars/tree-sitter-embedded-template/tree-sitter-embedded-template.wasm and b/packages/language-html/grammars/tree-sitter-embedded-template/tree-sitter-embedded-template.wasm differ diff --git a/packages/language-html/grammars/tree-sitter-html/tree-sitter-html.wasm b/packages/language-html/grammars/tree-sitter-html/tree-sitter-html.wasm index f71b6baa8..2d2835ca4 100755 Binary files a/packages/language-html/grammars/tree-sitter-html/tree-sitter-html.wasm and b/packages/language-html/grammars/tree-sitter-html/tree-sitter-html.wasm differ diff --git a/packages/language-html/lib/main.js b/packages/language-html/lib/main.js index 55cce8174..c0c230c9f 100644 --- a/packages/language-html/lib/main.js +++ b/packages/language-html/lib/main.js @@ -1,4 +1,4 @@ -exports.activate = function() { +exports.activate = function () { atom.grammars.addInjectionPoint('text.html.basic', { type: 'script_element', language() { @@ -19,38 +19,6 @@ exports.activate = function() { } }); - const TODO_PATTERN = /\b(TODO|FIXME|CHANGED|XXX|IDEA|HACK|NOTE|REVIEW|NB|BUG|QUESTION|COMBAK|TEMP|DEBUG|OPTIMIZE|WARNING)\b/; - const HYPERLINK_PATTERN = /\bhttps?:/ - - atom.grammars.addInjectionPoint('text.html.basic', { - type: 'comment', - language: (node) => { - return TODO_PATTERN.test(node.text) ? 'todo' : undefined; - }, - content: (node) => node, - languageScope: null - }); - - atom.grammars.addInjectionPoint('text.html.basic', { - type: 'comment', - language: (node) => { - return HYPERLINK_PATTERN.test(node.text) ? 'hyperlink' : undefined; - }, - content: (node) => node, - languageScope: null - }); - - atom.grammars.addInjectionPoint('text.html.basic', { - type: 'attribute_value', - language: (node) => { - return HYPERLINK_PATTERN.test(node.text) ? 'hyperlink' : undefined; - }, - content: (node) => node, - languageScope: null - }); - - // TODO: Inject hyperlink grammar into plain text? - // EMBEDDED atom.grammars.addInjectionPoint('text.html.ejs', { @@ -95,3 +63,14 @@ exports.activate = function() { } }); }; + +exports.consumeHyperlinkInjection = (hyperlink) => { + // TODO: Inject hyperlink grammar into plain text? + hyperlink.addInjectionPoint('text.html.basic', { + types: ['comment', 'attribute_value'] + }); +}; + +exports.consumeTodoInjection = (todo) => { + todo.addInjectionPoint('text.html.basic', { types: ['comment'] }); +}; diff --git a/packages/language-html/package.json b/packages/language-html/package.json index e2fd7b41b..721353260 100644 --- a/packages/language-html/package.json +++ b/packages/language-html/package.json @@ -19,5 +19,17 @@ }, "devDependencies": { "dedent": "^0.7.0" + }, + "consumedServices": { + "hyperlink.injection": { + "versions": { + "0.1.0": "consumeHyperlinkInjection" + } + }, + "todo.injection": { + "versions": { + "0.1.0": "consumeTodoInjection" + } + } } } diff --git a/packages/language-html/spec/.eslintrc.js b/packages/language-html/spec/.eslintrc.js new file mode 100644 index 000000000..5226d6921 --- /dev/null +++ b/packages/language-html/spec/.eslintrc.js @@ -0,0 +1,9 @@ +module.exports = { + env: { jasmine: true }, + rules: { + "node/no-unpublished-require": "off", + "node/no-extraneous-require": "off", + "no-unused-vars": "off", + "no-empty": "off" + } +}; diff --git a/packages/language-hyperlink/lib/main.js b/packages/language-hyperlink/lib/main.js new file mode 100644 index 000000000..16223af3a --- /dev/null +++ b/packages/language-hyperlink/lib/main.js @@ -0,0 +1,58 @@ + +const HYPERLINK_PATTERN = /\bhttps?:/ + +module.exports = { + provideHyperlinkInjection() { + return { + // Private: Test whether a Tree-sitter node's text contains any tokens + // that would benefit from a hyperlink injection. + // + // Useful if you want to call {GrammarRegistry::addInjectionPoint} + // yourself and want to use this logic in a `language` callback. + // + // * `node` A Tree-sitter tree node. + test(node) { + return HYPERLINK_PATTERN.test(node.text); + }, + + // Private: specify one or more types of syntax nodes for a given grammar + // that may embed the hyperlink grammar. + // + // * `scopeName` The {String} ID of the parent language. + // * `options` An {Object} with the following keys: + // * `types` An {Array} or {String} indicating the type or types of + // Tree-sitter tree nodes that may receive injections. + // * `language` (optional) A {Function} that may be called to add extra + // logic for determining which language should be used in an + // injection. If present, will be called before the default logic. + // If it returns `undefined`, the default logic will apply. If it + // returns a {String} or `null`, the default logic will be preempted. + // * `content` (optional) A {Function} that will be used to determine + // which of the injection node's children, if any, will be injected + // into. The default `content` callback is one that returns the + // original node. + addInjectionPoint(scopeName, options) { + let types = options.types; + if (!Array.isArray(types)) types = [types]; + + for (let type of types) { + atom.grammars.addInjectionPoint(scopeName, { + type, + language(node) { + if (options.language) { + let result = options.language(node); + if (result !== undefined) return result; + } + return HYPERLINK_PATTERN.test(node.text) ? + 'hyperlink' : undefined; + }, + content(node) { + return options.content ? options.content(node) : node; + }, + languageScope: null + }); + } + }, + } + } +}; diff --git a/packages/language-hyperlink/package.json b/packages/language-hyperlink/package.json index 676c2425f..9d1df06b3 100644 --- a/packages/language-hyperlink/package.json +++ b/packages/language-hyperlink/package.json @@ -1,11 +1,19 @@ { "name": "language-hyperlink", "version": "0.17.1", + "main": "lib/main", "description": "Hyperlink colorization in Atom", "engines": { "atom": "*", "node": ">=14" }, "repository": "https://github.com/pulsar-edit/pulsar", - "license": "MIT" + "license": "MIT", + "providedServices": { + "hyperlink.injection": { + "versions": { + "0.1.0": "provideHyperlinkInjection" + } + } + } } diff --git a/packages/language-hyperlink/spec/.eslintrc.js b/packages/language-hyperlink/spec/.eslintrc.js new file mode 100644 index 000000000..77888e061 --- /dev/null +++ b/packages/language-hyperlink/spec/.eslintrc.js @@ -0,0 +1,13 @@ +module.exports = { + env: { jasmine: true }, + globals: { + waitsForPromise: true, + advanceClock: true + }, + rules: { + "node/no-unpublished-require": "off", + "node/no-extraneous-require": "off", + "no-unused-vars": "off", + "no-empty": "off" + } +}; diff --git a/packages/language-java/grammars/tree-sitter-java/highlights.scm b/packages/language-java/grammars/tree-sitter-java/highlights.scm index d7644e1c3..86a06a56a 100644 --- a/packages/language-java/grammars/tree-sitter-java/highlights.scm +++ b/packages/language-java/grammars/tree-sitter-java/highlights.scm @@ -217,9 +217,13 @@ (identifier) @variable.parameter.lambda.java)) (variable_declarator - name: (identifier) @variable.other.assignment.java) + name: (identifier) @variable.other.declaration.java) +(assignment_expression + left: (identifier) @variable.other.assignment.java) +(update_expression + (identifier) @variable.other.assignment.java) ; PACKAGES ; ======== @@ -359,6 +363,8 @@ (binary_expression ["&" "|" "^" "~" "<<" ">>" ">>>"] @keyword.operator.bitwise.java) +["++" "--"] @keyword.operator.increment.java + "." @keyword.operator.accessor.dot.java "::" @keyword.operator.accessor.method-reference.java diff --git a/packages/language-java/lib/main.js b/packages/language-java/lib/main.js index a1e953a7a..59f36c832 100644 --- a/packages/language-java/lib/main.js +++ b/packages/language-java/lib/main.js @@ -1,24 +1,10 @@ -exports.activate = () => { - const TODO_PATTERN = /\b(TODO|FIXME|CHANGED|XXX|IDEA|HACK|NOTE|REVIEW|NB|BUG|QUESTION|COMBAK|TEMP|DEBUG|OPTIMIZE|WARNING)\b/; - const HYPERLINK_PATTERN = /\bhttps?:/ - atom.grammars.addInjectionPoint('source.java', { - type: 'comment', - language: (node) => { - return TODO_PATTERN.test(node.text) ? 'todo' : undefined; - }, - content: (node) => node, - languageScope: null +exports.consumeHyperlinkInjection = (hyperlink) => { + hyperlink.addInjectionPoint('source.java', { + types: ['comment', 'string_literal'] }); - - for (let type of ['string_literal', 'comment']) { - atom.grammars.addInjectionPoint('source.java', { - type, - language: (node) => { - return HYPERLINK_PATTERN.test(node.text) ? 'hyperlink' : undefined; - }, - content: (node) => node, - languageScope: null - }); - } +}; + +exports.consumeTodoInjection = (todo) => { + todo.addInjectionPoint('source.java', { types: ['comment'] }); }; diff --git a/packages/language-java/package.json b/packages/language-java/package.json index f330983e5..11667e670 100644 --- a/packages/language-java/package.json +++ b/packages/language-java/package.json @@ -11,5 +11,17 @@ "license": "MIT", "dependencies": { "tree-sitter-java": "0.19.1" + }, + "consumedServices": { + "hyperlink.injection": { + "versions": { + "0.1.0": "consumeHyperlinkInjection" + } + }, + "todo.injection": { + "versions": { + "0.1.0": "consumeTodoInjection" + } + } } } diff --git a/packages/language-javascript/grammars/tree-sitter-2-javascript.cson b/packages/language-javascript/grammars/modern-tree-sitter-javascript.cson similarity index 51% rename from packages/language-javascript/grammars/tree-sitter-2-javascript.cson rename to packages/language-javascript/grammars/modern-tree-sitter-javascript.cson index 41430d861..b2fc9fc6a 100644 --- a/packages/language-javascript/grammars/tree-sitter-2-javascript.cson +++ b/packages/language-javascript/grammars/modern-tree-sitter-javascript.cson @@ -4,13 +4,15 @@ type: 'modern-tree-sitter' parser: 'tree-sitter-javascript' injectionRegex: '^(js|javascript|JS|JAVASCRIPT)$' + treeSitter: - grammar: 'ts/grammar.wasm' - highlightsQuery: 'ts/highlights.scm' - localsQuery: 'ts/locals.scm' - foldsQuery: 'ts/folds.scm' - indentsQuery: 'ts/indents.scm' - tagsQuery: 'ts/tags.scm' + parserSource: 'github:tree-sitter/tree-sitter-javascript#f1e5a09b8d02f8209a68249c93f0ad647b228e6e' + grammar: 'tree-sitter/tree-sitter-javascript.wasm' + highlightsQuery: 'tree-sitter/highlights.scm' + localsQuery: 'tree-sitter/locals.scm' + foldsQuery: 'tree-sitter/folds.scm' + indentsQuery: 'tree-sitter/indents.scm' + tagsQuery: 'tree-sitter/tags.scm' firstLineRegex: [ # shebang line diff --git a/packages/language-javascript/grammars/modern-tree-sitter-jsdoc.cson b/packages/language-javascript/grammars/modern-tree-sitter-jsdoc.cson new file mode 100644 index 000000000..ab7df419f --- /dev/null +++ b/packages/language-javascript/grammars/modern-tree-sitter-jsdoc.cson @@ -0,0 +1,13 @@ +name: 'JSDoc' +scopeName: 'source.jsdoc' +type: 'modern-tree-sitter' +parser: 'tree-sitter-jsdoc' + +injectionRegex: '^jsdoc$' + +treeSitter: + parserSource: 'github:tree-sitter/tree-sitter-jsdoc#a5e363a98676136d9f5884cb558086e5f1fc32b6' + grammar: 'tree-sitter/jsdoc/tree-sitter-jsdoc.wasm' + highlightsQuery: 'tree-sitter/jsdoc/highlights.scm' + foldsQuery: 'tree-sitter/jsdoc/folds.scm' + indentsQuery: 'tree-sitter/jsdoc/indents.scm' diff --git a/packages/language-javascript/grammars/modern-tree-sitter-regex.cson b/packages/language-javascript/grammars/modern-tree-sitter-regex.cson new file mode 100644 index 000000000..f79ba9352 --- /dev/null +++ b/packages/language-javascript/grammars/modern-tree-sitter-regex.cson @@ -0,0 +1,11 @@ +name: 'JavaScript RegExp' +scopeName: 'source.js.regexp' +type: 'modern-tree-sitter' +parser: 'tree-sitter-regex' + +injectionRegex: '^(js-regex)$' + +treeSitter: + parserSource: 'github:tree-sitter/tree-sitter-regex#2354482d7e2e8f8ff33c1ef6c8aa5690410fbc96' + grammar: 'tree-sitter/regex/tree-sitter-regex.wasm' + highlightsQuery: 'tree-sitter/regex/highlights.scm' diff --git a/packages/language-javascript/grammars/tree-sitter-2-jsdoc.cson b/packages/language-javascript/grammars/tree-sitter-2-jsdoc.cson deleted file mode 100644 index cd1a6be5b..000000000 --- a/packages/language-javascript/grammars/tree-sitter-2-jsdoc.cson +++ /dev/null @@ -1,12 +0,0 @@ -name: 'JSDoc' -scopeName: 'source.jsdoc' -type: 'modern-tree-sitter' -parser: 'tree-sitter-jsdoc' - -injectionRegex: '^jsdoc$' - -treeSitter: - grammar: 'ts/jsdoc/tree-sitter-jsdoc.wasm' - highlightsQuery: 'ts/jsdoc/highlights.scm' - foldsQuery: 'ts/jsdoc/folds.scm' - indentsQuery: 'ts/jsdoc/indents.scm' diff --git a/packages/language-javascript/grammars/tree-sitter-2-regex.cson b/packages/language-javascript/grammars/tree-sitter-2-regex.cson deleted file mode 100644 index 0ac0d535d..000000000 --- a/packages/language-javascript/grammars/tree-sitter-2-regex.cson +++ /dev/null @@ -1,9 +0,0 @@ -name: 'JavaScript RegExp' -scopeName: 'source.js.regexp' -type: 'modern-tree-sitter' -parser: 'tree-sitter-regex' - -injectionRegex: '^(js-regex)$' -treeSitter: - grammar: 'ts/regex/tree-sitter-regex.wasm' - highlightsQuery: 'ts/regex/highlights.scm' diff --git a/packages/language-javascript/grammars/ts/folds.scm b/packages/language-javascript/grammars/tree-sitter/folds.scm similarity index 96% rename from packages/language-javascript/grammars/ts/folds.scm rename to packages/language-javascript/grammars/tree-sitter/folds.scm index 0d5f95ae1..5035311ca 100644 --- a/packages/language-javascript/grammars/ts/folds.scm +++ b/packages/language-javascript/grammars/tree-sitter/folds.scm @@ -80,4 +80,4 @@ ((jsx_self_closing_element) @fold ; Exclude both the slash and angle bracket `/>` from the fold. - (#set! fold.endAt lastChild.previousSibling.startPosition)) + (#set! fold.endAt lastChild.startPosition)) diff --git a/packages/language-javascript/grammars/ts/highlights.scm b/packages/language-javascript/grammars/tree-sitter/highlights.scm similarity index 94% rename from packages/language-javascript/grammars/ts/highlights.scm rename to packages/language-javascript/grammars/tree-sitter/highlights.scm index cd067c58d..16d8dd99d 100644 --- a/packages/language-javascript/grammars/ts/highlights.scm +++ b/packages/language-javascript/grammars/tree-sitter/highlights.scm @@ -71,17 +71,26 @@ (assignment_expression left: (identifier) @variable.other.assignment.js) -; The "bar" in `foo.bar = true` +; Mark all the properties whose right-hand sides are functions so that we can +; exclude them from the next query. (assignment_expression left: (member_expression - property: (property_identifier) @variable.other.assignment.property.js)) + property: (property_identifier) @variable.other.assignment.property.js) + right: [(arrow_function) (function)] @_IGNORE_ + (#set! isFunctionProperty true)) -; The "bar" in `foo.#bar = true` +; The "bar" in `foo.bar = true`. +(assignment_expression + left: (member_expression + property: (property_identifier) @variable.other.assignment.property.js) + (#is-not? test.rangeWithData isFunctionProperty) + (#set! capture.final)) + +; The "bar" in `foo.#bar = true`. (assignment_expression left: (member_expression property: (private_property_identifier) @variable.other.assignment.property.private.js)) - ; The "foo" in `foo += 1`. (augmented_assignment_expression left: (identifier) @variable.other.assignment.js) @@ -90,6 +99,13 @@ (update_expression argument: (identifier) @variable.other.assignment.js) +; Public field definition in a class body: +; The "foo" in `foo = "bar";` +(field_definition + property: (property_identifier) @variable.other.assignment.property.public.js) + +; Private field definition in a class body: +; The "#foo" in `#foo = "bar";` (field_definition property: (private_property_identifier) @variable.other.assignment.property.private.js) @@ -139,18 +155,17 @@ (rest_pattern (identifier) @variable.other.assignment.destructuring.rest.js)) -; A variable array destructuring: -; The "foo" and "bar" in `let [foo, bar] = something` -(variable_declarator - (array_pattern +; An array-destructured assignment or reassignment, regardless of depth: +; The "foo" in `[foo] = bar;` and `[[foo]] = bar;`. +(array_pattern + (identifier) @variable.other.assignment.destructuring.js) + +; An array-destructured assignment or reassignment with a default, regardless of depth: +; The "baz" in `let [foo, bar, baz = false] = something;` and `let [[baz = 5]] = something`; +(array_pattern + (assignment_pattern (identifier) @variable.other.assignment.destructuring.js)) -; A variable array destructuring with a default: -; The "baz" in `let [foo, bar, baz = false] = something` -(variable_declarator - (array_pattern - (assignment_pattern - (identifier) @variable.other.assignment.destructuring.js))) ; A variable declaration in a for…(in|of) loop: ; The "foo" in `for (let foo of bar) {` @@ -250,6 +265,11 @@ (method_definition name: (property_identifier) @entity.name.function.method.definition.js) +; Private method definitions: +; the "#foo" in `#foo () {` (inside a class body) +(method_definition + name: (private_property_identifier) @entity.name.function.method.private.definition.js) + ; Function property assignment: ; The "foo" in `thing.foo = (arg) => {}` (assignment_expression @@ -744,7 +764,7 @@ ; The "Foo" in ``. (jsx_self_closing_element name: (identifier) @entity.name.tag.js - ) @meta.tag.js + ) @meta.tag.jsx.js ; The "Foo" in ``. (jsx_opening_element @@ -752,8 +772,6 @@ ; The "Foo" in ``. (jsx_closing_element - "/" @punctuation.definition.tag.end.js - (#set! capture.final true) name: (identifier) @entity.name.tag.js) ; The "bar" in ``. @@ -769,23 +787,18 @@ (jsx_opening_element "<" @punctuation.definition.tag.begin.js - ">" @punctuation.definition.tag.end.js) + ">" @punctuation.definition.tag.end.js) @meta.tag.jsx.js (jsx_closing_element - "<" @punctuation.definition.tag.begin.js - ">" @punctuation.definition.tag.end.js) + "" @punctuation.definition.tag.end.js) @meta.tag.jsx.js (jsx_self_closing_element "<" @punctuation.definition.tag.begin.js (#set! capture.final true)) -((jsx_self_closing_element - ; The "/>" in ``, extended to cover both anonymous nodes at once. - "/") @punctuation.definition.tag.end.js - (#set! adjust.startAt lastChild.previousSibling.startPosition) - (#set! adjust.endAt lastChild.endPosition) - (#set! capture.final true)) - +(jsx_self_closing_element + "/>" @punctuation.definition.tag.end.js) ; OPERATORS ; ========== @@ -812,7 +825,13 @@ (unary_expression ["+" "-"] @keyword.operator.unary.js) -(ternary_expression ["?" ":"] @keyword.operator.ternary.js) +(ternary_expression ["?" ":"] @keyword.operator.ternary.js + (#set! capture.final)) + +; Try to highlight `?` like an operator while the user is typing without +; waiting for its paired `:`. +("?" @keyword.operator.ternary.js + (#is? test.descendantOfType "ERROR")) [ "&&=" diff --git a/packages/language-javascript/grammars/ts/indents.scm b/packages/language-javascript/grammars/tree-sitter/indents.scm similarity index 60% rename from packages/language-javascript/grammars/ts/indents.scm rename to packages/language-javascript/grammars/tree-sitter/indents.scm index dfac5ef7e..b351b7bcf 100644 --- a/packages/language-javascript/grammars/ts/indents.scm +++ b/packages/language-javascript/grammars/tree-sitter/indents.scm @@ -1,7 +1,3 @@ - -; ((template_string) @ignore - ; (#is-not? test.OnStartingOrEndingRow true)) - ; STATEMENT BLOCKS ; ================ @@ -20,11 +16,19 @@ (#is? test.last true)) (#set! indent.matchIndentOf parent.startPosition)) -; 'case' and 'default' need to be indented one level more than their containing -; `switch`. TODO: Might need to make this configurable. +; By default, `case` and `default` need to be indented one level more than their containing +; `switch`. (["case" "default"] @match (#set! indent.matchIndentOf parent.parent.startPosition) - (#set! indent.offsetIndent 1)) + (#set! indent.offsetIndent 1) + (#is-not? test.config "language-javascript.indentation.alignCaseWithSwitch")) + +; When this config setting is enabled, `case` and `default` need to be indented +; to match their containing `switch`. +(["case" "default"] @match + (#set! indent.matchIndentOf parent.parent.startPosition) + (#set! indent.offsetIndent 0) + (#is? test.config "language-javascript.indentation.alignCaseWithSwitch")) ; ONE-LINE CONDITIONALS @@ -33,10 +37,12 @@ ; An `if` statement without an opening brace should indent the next line… (if_statement condition: (parenthesized_expression ")" @indent - (#is? test.lastTextOnRow true))) + (#is? test.lastTextOnRow true) + (#is? test.config "language-javascript.indentation.indentAfterBracelessIf"))) ; (as should a braceless `else`…) ("else" @indent - (#is? test.lastTextOnRow true)) + (#is? test.lastTextOnRow true) + (#is? test.config "language-javascript.indentation.indentAfterBracelessIf")) ; …and keep that indent level if the user types a comment before the ; consequence… @@ -44,7 +50,8 @@ consequence: (empty_statement) @match (#is-not? test.startsOnSameRowAs parent.startPosition) (#set! indent.matchIndentOf parent.startPosition) - (#set! indent.offsetIndent 1)) + (#set! indent.offsetIndent 1) + (#is? test.config "language-javascript.indentation.indentAfterBracelessIf")) ; …and keep that indent level after the user starts typing… (if_statement @@ -61,7 +68,8 @@ ; of an `expression_statement`, for some reason. (#not-match? @match "^\\s*{") (#set! indent.matchIndentOf parent.startPosition) - (#set! indent.offsetIndent 1)) + (#set! indent.offsetIndent 1) + (#is? test.config "language-javascript.indentation.indentAfterBracelessIf")) ; …but dedent after exactly one statement. (if_statement @@ -76,7 +84,8 @@ ] @dedent.next ; When an opening curly brace is unpaired, it might get interpreted as part ; of an `expression_statement`, for some reason. - (#not-match? @dedent.next "^\\s*{")) + (#not-match? @dedent.next "^\\s*{") + (#is? test.config "language-javascript.indentation.indentAfterBracelessIf")) (else_clause [ @@ -87,7 +96,8 @@ (throw_statement) (debugger_statement) ] @dedent.next - (#is-not? test.startsOnSameRowAs parent.startPosition)) + (#is-not? test.startsOnSameRowAs parent.startPosition) + (#is? test.config "language-javascript.indentation.indentAfterBracelessIf")) ; HANGING INDENT ON SPLIT LINES @@ -97,14 +107,22 @@ ; `config` scope test. ; Any of these at the end of a line indicate the next line should be indented… -(["||" "&&" "?"] @indent +(["||" "&&"] @indent + (#is? test.config "language-javascript.indentation.addHangingIndentAfterLogicalOperators") + (#is? test.lastTextOnRow true)) + +("?" @indent + (#is? test.config "language-javascript.indentation.addHangingIndentAfterTernaryOperators") (#is? test.lastTextOnRow true)) ; …and the line after that should be dedented… (binary_expression ["||" "&&"] right: (_) @dedent.next - (#is-not? test.startsOnSameRowAs parent.startPosition)) + (#is? test.config "language-javascript.indentation.addHangingIndentAfterLogicalOperators") + (#is-not? test.startsOnSameRowAs parent.startPosition) + ; …unless the right side of the expression spans multiple lines. + (#is? test.endsOnSameRowAs startPosition)) ; …unless it's a ternary, in which case the dedent should wait until the ; alternative clause. @@ -115,20 +133,11 @@ ; (ternary_expression alternative: (_) @dedent.next - (#is-not? test.startsOnSameRowAs parent.startPosition)) - - -; DEDENT-NEXT IN LIMITED SCENARIOS -; ================================ - -; Catches unusual hanging-indent scenarios when calling a method, such as: -; -; return this.veryLongMethodNameWithSeveralArgumentsThat(are, too, -; short, forEach, toHave, itsOwn, line); -; -; (arguments ")" @dedent.next -; (#is-not? test.startsOnSameRowAs parent.firstChild.startPosition) -; (#is-not? test.firstTextOnRow true)) + (#is? test.config "language-javascript.indentation.addHangingIndentAfterTernaryOperators") + (#is-not? test.startsOnSameRowAs parent.startPosition) + ; Only dedent the next line if the alternative doesn't itself span multiple + ; lines. + (#is? test.endsOnSameRowAs startPosition)) ; GENERAL @@ -137,21 +146,27 @@ ; Weed out `}`s that should not signal dedents. (template_substitution "}" @_IGNORE_ (#set! capture.final true)) -[ - "{" - "(" - "[" -] @indent +; As strange as it may seem to make all of these basic indentation hints +; configurable, some brace styles are incompatible with some of these choices; +; see https://github.com/orgs/pulsar-edit/discussions/249. +("{" @indent + (#is? test.config "language-javascript.indentation.indentBraces")) +("}" @dedent + (#is? test.config "language-javascript.indentation.indentBraces")) + +("[" @indent + (#is? test.config "language-javascript.indentation.indentBrackets")) +("]" @dedent + (#is? test.config "language-javascript.indentation.indentBrackets")) + +("(" @indent + (#is? test.config "language-javascript.indentation.indentParentheses")) +(")" @dedent + (#is? test.config "language-javascript.indentation.indentParentheses")) -[ - "}" - ")" - "]" -] @dedent ["case" "default"] @indent - ; JSX ; === @@ -186,7 +201,7 @@ ; point, so the usual heuristic won't work. Instead we set `indent.force` and ; use `test.lastTextOnRow` to ensure that the dedent fires exactly once while ; typing. -((jsx_self_closing_element ">" @dedent) +((jsx_self_closing_element "/>" @dedent) (#is-not? test.startsOnSameRowAs parent.firstChild.startPosition) (#is? test.lastTextOnRow) (#set! indent.force true)) diff --git a/packages/language-javascript/grammars/ts/jsdoc/folds.scm b/packages/language-javascript/grammars/tree-sitter/jsdoc/folds.scm similarity index 100% rename from packages/language-javascript/grammars/ts/jsdoc/folds.scm rename to packages/language-javascript/grammars/tree-sitter/jsdoc/folds.scm diff --git a/packages/language-javascript/grammars/ts/jsdoc/highlights.scm b/packages/language-javascript/grammars/tree-sitter/jsdoc/highlights.scm similarity index 96% rename from packages/language-javascript/grammars/ts/jsdoc/highlights.scm rename to packages/language-javascript/grammars/tree-sitter/jsdoc/highlights.scm index 10d05b3a0..29b84d02c 100644 --- a/packages/language-javascript/grammars/ts/jsdoc/highlights.scm +++ b/packages/language-javascript/grammars/tree-sitter/jsdoc/highlights.scm @@ -21,7 +21,7 @@ ((inline_tag) @meta.inline-tag.jsdoc.js) -(tag_name) @entity.name.tag.jsdoc.js +(tag_name) @keyword.other.tag.jsdoc.js ((tag (type)) @storage.type.instance.jsdoc.js ; Join the type with its surrounding braces. diff --git a/packages/language-javascript/grammars/ts/jsdoc/indents.scm b/packages/language-javascript/grammars/tree-sitter/jsdoc/indents.scm similarity index 100% rename from packages/language-javascript/grammars/ts/jsdoc/indents.scm rename to packages/language-javascript/grammars/tree-sitter/jsdoc/indents.scm diff --git a/packages/language-javascript/grammars/tree-sitter/jsdoc/tree-sitter-jsdoc.wasm b/packages/language-javascript/grammars/tree-sitter/jsdoc/tree-sitter-jsdoc.wasm new file mode 100755 index 000000000..d16945f9e Binary files /dev/null and b/packages/language-javascript/grammars/tree-sitter/jsdoc/tree-sitter-jsdoc.wasm differ diff --git a/packages/language-javascript/grammars/ts/locals.scm b/packages/language-javascript/grammars/tree-sitter/locals.scm similarity index 100% rename from packages/language-javascript/grammars/ts/locals.scm rename to packages/language-javascript/grammars/tree-sitter/locals.scm diff --git a/packages/language-ruby/grammars/ts/regex/highlights.scm b/packages/language-javascript/grammars/tree-sitter/regex/highlights.scm similarity index 81% rename from packages/language-ruby/grammars/ts/regex/highlights.scm rename to packages/language-javascript/grammars/tree-sitter/regex/highlights.scm index b0018b6c7..3f79debaf 100644 --- a/packages/language-ruby/grammars/ts/regex/highlights.scm +++ b/packages/language-javascript/grammars/tree-sitter/regex/highlights.scm @@ -1,8 +1,3 @@ -; CAVEATS: -; -; * No support for lookbehind as of March 2023 (waiting on -; https://github.com/tree-sitter/tree-sitter-regex/pull/15) - (non_capturing_group) @meta.group.non-capturing.regexp [ @@ -17,6 +12,8 @@ [ (boundary_assertion) + (start_assertion) + (end_assertion) ] @keyword.control.anchor.regexp [ @@ -24,10 +21,10 @@ (lazy) ] @keyword.operator.quantifier.regexp -((lookahead_assertion) @keyword.operator.lookahead.regexp +((lookaround_assertion) @keyword.operator.lookaround.regexp (#set! adjust.startAndEndAroundFirstMatchOf "\\?=")) -((lookahead_assertion) @keyword.operator.lookahead.negated.regexp +((lookaround_assertion) @keyword.operator.lookaround.negated.regexp (#set! adjust.startAndEndAroundFirstMatchOf "\\?!")) ((non_capturing_group) @keyword.operator.group.non-capturing.regexp diff --git a/packages/language-javascript/grammars/tree-sitter/regex/tree-sitter-regex.wasm b/packages/language-javascript/grammars/tree-sitter/regex/tree-sitter-regex.wasm new file mode 100755 index 000000000..dd1988d42 Binary files /dev/null and b/packages/language-javascript/grammars/tree-sitter/regex/tree-sitter-regex.wasm differ diff --git a/packages/language-javascript/grammars/ts/tags.scm b/packages/language-javascript/grammars/tree-sitter/tags.scm similarity index 100% rename from packages/language-javascript/grammars/ts/tags.scm rename to packages/language-javascript/grammars/tree-sitter/tags.scm diff --git a/packages/language-javascript/grammars/tree-sitter/tree-sitter-javascript.wasm b/packages/language-javascript/grammars/tree-sitter/tree-sitter-javascript.wasm new file mode 100755 index 000000000..5857388d3 Binary files /dev/null and b/packages/language-javascript/grammars/tree-sitter/tree-sitter-javascript.wasm differ diff --git a/packages/language-javascript/grammars/ts/grammar.wasm b/packages/language-javascript/grammars/ts/grammar.wasm deleted file mode 100755 index 76c513e17..000000000 Binary files a/packages/language-javascript/grammars/ts/grammar.wasm and /dev/null differ diff --git a/packages/language-javascript/grammars/ts/jsdoc/tree-sitter-jsdoc.wasm b/packages/language-javascript/grammars/ts/jsdoc/tree-sitter-jsdoc.wasm deleted file mode 100755 index 7b9bde3d6..000000000 Binary files a/packages/language-javascript/grammars/ts/jsdoc/tree-sitter-jsdoc.wasm and /dev/null differ diff --git a/packages/language-javascript/grammars/ts/regex/tree-sitter-regex.wasm b/packages/language-javascript/grammars/ts/regex/tree-sitter-regex.wasm deleted file mode 100755 index 553c23b6b..000000000 Binary files a/packages/language-javascript/grammars/ts/regex/tree-sitter-regex.wasm and /dev/null differ diff --git a/packages/language-javascript/lib/main.js b/packages/language-javascript/lib/main.js index ed04e1978..015c289b6 100644 --- a/packages/language-javascript/lib/main.js +++ b/packages/language-javascript/lib/main.js @@ -57,19 +57,6 @@ exports.activate = function () { languageScope: null }); - // TODO: Ideal would be to have one `language-todo` injection for the whole - // document responsible for highlighting TODOs in all comments, but - // performance needs to be better than it is now for that to be possible. - // Injecting into individual line comments results in less time parsing - // during buffer modification, but _lots_ of language layers. - // - // Compromise is to test the content first and then only inject a layer for - // `language-todo` when we know it'll be needed. All this also applies for - // `language-hyperlink`. - // - const TODO_PATTERN = /\b(TODO|FIXME|CHANGED|XXX|IDEA|HACK|NOTE|REVIEW|NB|BUG|QUESTION|COMBAK|TEMP|DEBUG|OPTIMIZE|WARNING)\b/; - const HYPERLINK_PATTERN = /\bhttps?:/ - atom.grammars.addInjectionPoint('source.js', { type: 'comment', language(comment) { @@ -81,28 +68,16 @@ exports.activate = function () { languageScope: null, coverShallowerScopes: true }); +}; - // Experiment: better to have one layer with lots of nodes, or lots of - // layers each managing one node? - atom.grammars.addInjectionPoint('source.js', { - type: 'comment', - language: (node) => { - return TODO_PATTERN.test(node.text) ? 'todo' : undefined; - }, - content: (node) => node, - languageScope: null +exports.consumeHyperlinkInjection = (hyperlink) => { + hyperlink.addInjectionPoint('source.js', { + types: ['comment', 'template_string', 'string_fragment'] }); +}; - for (let type of ['template_string', 'string_fragment', 'comment']) { - atom.grammars.addInjectionPoint('source.js', { - type, - language: (node) => { - return HYPERLINK_PATTERN.test(node.text) ? 'hyperlink' : undefined; - }, - content: (node) => node, - languageScope: null - }); - } +exports.consumeTodoInjection = (todo) => { + todo.addInjectionPoint('source.js', { types: ['comment'] }); }; const CSS_REGEX = /\bstyled\b|\bcss\b/i; diff --git a/packages/language-javascript/package.json b/packages/language-javascript/package.json index 5f8c71c82..63b13e6a4 100644 --- a/packages/language-javascript/package.json +++ b/packages/language-javascript/package.json @@ -16,5 +16,74 @@ "tree-sitter-javascript": "0.19.0", "tree-sitter-jsdoc": "0.19.0", "tree-sitter-regex": "0.19.0" + }, + "configSchema": { + "indentation": { + "title": "Indentation", + "type": "object", + "properties": { + "indentBraces": { + "title": "Indent Curly Braces", + "type": "boolean", + "default": true, + "order": 1, + "description": "Indent after `{`." + }, + "indentBrackets": { + "title": "Indent Brackets", + "type": "boolean", + "default": true, + "order": 2, + "description": "Indent after `[`." + }, + "indentParentheses": { + "title": "Indent Parentheses", + "type": "boolean", + "default": true, + "order": 3, + "description": "Indent after `(`." + }, + "alignCaseWithSwitch": { + "title": "Align “case” With ”switch”", + "type": "boolean", + "default": false, + "order": 4, + "description": "When enabled, `case` and `default` statements in `switch` blocks will match the indent level of the enclosing `switch` instead of indenting themselves one level." + }, + "indentAfterBracelessIf": { + "title": "Indent After Braceless “if” And “else”", + "type": "boolean", + "default": true, + "order": 5, + "description": "When enabled, `if` and `else` statements without a brace on the initial line will trigger an indent, then a dedent after a single statement. Disable if your brace style is incompatible with this pattern." + }, + "addHangingIndentAfterLogicalOperators": { + "title": "Add Hanging Indent After Logical Operators", + "type": "boolean", + "default": true, + "order": 6, + "description": "When enabled, will add a hanging indent when a line ends with `&&` or `||`, continuing the indent until the end of the statement." + }, + "addHangingIndentAfterTernaryOperators": { + "title": "Add Hanging Indent After Ternary Operators", + "type": "boolean", + "default": true, + "order": 7, + "description": "When enabled, will add a hanging indent when a line ends with `?`, continuing the indent through the ensuing `:` until the end of the statement." + } + } + } + }, + "consumedServices": { + "hyperlink.injection": { + "versions": { + "0.1.0": "consumeHyperlinkInjection" + } + }, + "todo.injection": { + "versions": { + "0.1.0": "consumeTodoInjection" + } + } } } diff --git a/packages/language-php/grammars/modern-tree-sitter-php-html.cson b/packages/language-php/grammars/modern-tree-sitter-php-html.cson index 2946872b4..77b49260f 100644 --- a/packages/language-php/grammars/modern-tree-sitter-php-html.cson +++ b/packages/language-php/grammars/modern-tree-sitter-php-html.cson @@ -6,12 +6,8 @@ parser: 'tree-sitter-php' injectionRegex: 'php|PHP' treeSitter: - parserSource: 'github:tree-sitter/tree-sitter-php#d5e7cacb6c27e0e131c7f76c0dbfee56dfcc61e3' + parserSource: 'github:tree-sitter/tree-sitter-php#b569a5f2c0d592e67430520d1a0e1f765d83ceb0' grammar: 'tree-sitter/tree-sitter-php.wasm' - highlightsQuery: 'tree-sitter/queries/highlights.scm' - tagsQuery: 'tree-sitter/queries/tags.scm' - foldsQuery: 'tree-sitter/queries/folds.scm' - indentsQuery: 'tree-sitter/queries/indents.scm' fileTypes: [ 'aw' @@ -28,7 +24,3 @@ fileTypes: [ 'phtml' 'profile' ] - -firstLineRegex: "^\\#!.*(?:\\s|\\/)php\\d?(?:$|\\s)|^\\s*<\\?(php|=|\\s|$)" - -contentRegex: "<\\?(php|=|\\s|$)" diff --git a/packages/language-php/grammars/modern-tree-sitter-php.cson b/packages/language-php/grammars/modern-tree-sitter-php.cson index 01b82d73c..c0014ee84 100644 --- a/packages/language-php/grammars/modern-tree-sitter-php.cson +++ b/packages/language-php/grammars/modern-tree-sitter-php.cson @@ -3,10 +3,13 @@ type: 'modern-tree-sitter' parser: 'tree-sitter-php' # Give it a precise injectionRegex that won't get accidentally matched with -# anything. This grammar only exists as a way to apply the `source.php` scope. +# anything. injectionRegex: '^(internal-php)$' treeSitter: - parserSource: 'github:tree-sitter/tree-sitter-php#594b8bad093abe739c3d2a2cae5abae33c5fb23d' + parserSource: 'github:tree-sitter/tree-sitter-php#b569a5f2c0d592e67430520d1a0e1f765d83ceb0' grammar: 'tree-sitter/tree-sitter-php.wasm' - highlightsQuery: 'tree-sitter/queries/empty.scm' + highlightsQuery: 'tree-sitter/queries/highlights.scm' + tagsQuery: 'tree-sitter/queries/tags.scm' + foldsQuery: 'tree-sitter/queries/folds.scm' + indentsQuery: 'tree-sitter/queries/indents.scm' diff --git a/packages/language-php/grammars/tree-sitter/queries/highlights.scm b/packages/language-php/grammars/tree-sitter/queries/highlights.scm index 5801f81a6..0af6dde5d 100644 --- a/packages/language-php/grammars/tree-sitter/queries/highlights.scm +++ b/packages/language-php/grammars/tree-sitter/queries/highlights.scm @@ -7,14 +7,35 @@ ; SUPPORT ; ======= +; There are lots of constructs that look like ordinary function calls but are +; actually special language statements. (array_creation_expression - "array" @support.function.builtin.array.php) + "array" @support.function.builtin.array.php + "(" @punctuation.definition.parameters.begin.bracket.round.php + ")" @punctuation.definition.parameters.end.bracket.round.php) -(list_literal "list" @support.function.builtin.list.php) +(list_literal "list" @support.function.builtin.list.php + "(" @punctuation.definition.parameters.begin.bracket.round.php + ")" @punctuation.definition.parameters.end.bracket.round.php) + +(unset_statement + "unset" @support.function.unset.php + "(" @punctuation.definition.parameters.begin.bracket.round.php + ")" @punctuation.definition.parameters.end.bracket.round.php) + +(print_intrinsic + ; Don't delimit the parentheses like parameter punctuation; they're optional + ; for `print`. + "print" @support.function.print.php) ; The list of standard library methods in `php.cson` is… a lot. This is my ; biased attempt to pare it down to the most important functions. +(function_call_expression + function: (name) @support.function._TEXT_.php + (#match? @support.function._TEXT_.php "^(isset|eval|empty)$") + (#set! capture.final)) + (function_call_expression function: (name) @support.function.array.php (#match? @support.function.array.php "^(shuffle|sizeof|sort|next|nat(case)?sort|count|compact|current|in_array|usort|uksort|uasort|pos|prev|end|each|extract|ksort|key(_exists)?|krsort|list|asort|arsort|rsort|reset|range|array(_(shift|sum|splice|search|slice|chunk|change_key_case|count_values|column|combine|(diff|intersect)(_(u)?(key|assoc))?|u(diff|intersect)(_(u)?assoc)?|unshift|unique|pop|push|pad|product|values|keys|key_exists|filter|fill(_keys)?|flip|walk(_recursive)?|reduce|replace(_recursive)?|reverse|rand|multisort|merge(_recursive)?|map)?))$")) @@ -31,10 +52,6 @@ function: (name) @support.function.class-obj.php (#match? @support.function.class-obj.php "^(class_alias|all_user_method(_array)?|is_(a|subclass_of)|__autoload|(class|interface|method|property|trait)_exists|get_(class(_(vars|methods))?|(called|parent)_class|object_vars|declared_(classes|interfaces|traits)))$")) -(function_call_expression - function: (name) @support.function.construct.php - (#match? @support.function.construct.php "^(isset|unset|eval|empty)$")) - (function_call_expression function: (name) @support.function.construct.output.php (#match? @support.function.construct.output.php "^(print|echo)$")) @@ -101,7 +118,7 @@ (function_call_expression function: (name) @support.function.math.php - (#match? @support.function.math.php "^((a)?(cos|sin|tan)(h)?|sqrt|srand|hypot|hexdec|ceil|is_(nan|(in)?finite)|octdec|dec(hex|oct|bin)|deg2rad|pi|pow|exp(m1)?|floor|fmod|lcg_value|log(1(p|0))?|atan2|abs|round|rand|rad2deg|getrandmax|mt_(srand|rand|getrandmax)|max|min|bindec|base_convert)$")) + (#match? @support.function.math.php "^((a)?(cos|sin|tan)(h)?|sqrt|srand|hypot|hexdec|ceil|is_(nan|(in)?finite)|octdec|dec(hex|oct|bin)|deg2rad|pi|pow|exp(m1)?|floor|f(mod|div)|lcg_value|log(1(p|0))?|atan2|abs|round|rand|rad2deg|getrandmax|mt_(srand|rand|getrandmax)|max|min|bindec|base_convert|intdiv)$")) (function_call_expression function: (name) @support.function.mbstring.php @@ -263,14 +280,15 @@ (class_constant_access_expression . (name) @support.class.php) (class_constant_access_expression (name) @support.other.property.php .) -; The "Foo" and "bar" in "Foo::bar()". -(scoped_call_expression - scope: (name) @support.class.php - name: (name) @support.other.function.method.static.php) - -; The "Foo" and "$bar" in "Foo::$bar()". +; The "Foo" in `Foo::bar()` and `Foo::$bar()`. (scoped_call_expression scope: (name) @support.class.php) + +; The "bar" in `Foo::bar()`. +(scoped_call_expression + name: (name) @support.other.function.method.static.php) + +; The "$bar" in `Foo::$bar()`. (scoped_call_expression name: (variable_name) @variable.other.method.static.php) @@ -300,6 +318,10 @@ name: (variable_name) @variable.other.property.php (#set! capture.final true)) +; The "Foo" in `new Foo();`. +(object_creation_expression + (name) @support.class.php) + ; TRAITS ; ====== @@ -316,13 +338,17 @@ ; TYPES ; ===== -(primitive_type) @storage.type.builtin.php -(cast_type) @storage.type.builtin.php -(named_type (name) @storage.type.php) -(named_type (qualified_name) @storage.type.php) +; Primitive types are value types, hence are placed in `support.storage.type`. +(primitive_type) @support.storage.type.builtin.php +(cast_type) @support.storage.type.builtin.php +(named_type (name) @support.storage.type.php) +(named_type (qualified_name) @support.storage.type.php) + +; Acts as a modifier on all variables, regardless of value type, hence `storage.modifier`. "global" @storage.modifier.global.php +; Core language constructs go in `storage.type`. ["enum" "interface" "trait" "class"] @storage.type._TYPE_.php (enum_case "case" @storage.type.case.php) "function" @storage.type.function.php @@ -376,8 +402,10 @@ ((dynamic_variable_name) @punctuation.definition.variable.begin.php (#set! adjust.startBeforeFirstMatchOf "^\\}$")) -((name) @constant.other.php - (#match? @constant.other.php "^_?[A-Z][A-Z\\d_]+$")) +; ((name) @constant.other.php +; (#match? @constant.other.php "^_?[A-Z][A-Z\\d_]+$")) + +(const_declaration (const_element) @variable.other.constant.php) ((name) @constant.language.php (#match? @constant.language.php "^__[A-Z][A-Z\d_]+__$")) @@ -483,11 +511,16 @@ (#match? @punctuation.definition.comment.php "^#") (#set! adjust.startAndEndAroundFirstMatchOf "^#")) -; Don't highlight PHPDoc comments because the injection will handle them. +; Capture these because the PHPDoc injection won't process them… +((comment) @comment.block.documentation.php + (#match? @comment.block.documentation.php "^/\\*\\*\\*")) + +; …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 "^/\\*(?!\\*)")) @@ -588,8 +621,10 @@ "{" @punctuation.definition.block.begin.bracket.curly.php "}" @punctuation.definition.block.end.bracket.curly.php -"(" @punctuation.definition.begin.bracket.round.php -")" @punctuation.definition.end.bracket.round.php +("(" @punctuation.definition.begin.bracket.round.php + (#set! capture.shy true)) +(")" @punctuation.definition.end.bracket.round.php + (#set! capture.shy true)) "[" @punctuation.definition.begin.bracket.square.php "]" @punctuation.definition.end.bracket.square.php diff --git a/packages/language-php/grammars/tree-sitter/queries/indents.scm b/packages/language-php/grammars/tree-sitter/queries/indents.scm index c7f198a40..2b888d187 100644 --- a/packages/language-php/grammars/tree-sitter/queries/indents.scm +++ b/packages/language-php/grammars/tree-sitter/queries/indents.scm @@ -1,8 +1,9 @@ -; + ["{" "(" "["] @indent ["}" ")" "]"] @dedent -":" @indent +; if ($foo): +(colon_block ":" @indent) ["endif" "endfor" "endforeach" "enddeclare" "endswitch"] @dedent 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 e9ee2c4d6..dfb4fee84 100644 --- a/packages/language-php/grammars/tree-sitter/queries/phpdoc/highlights.scm +++ b/packages/language-php/grammars/tree-sitter/queries/phpdoc/highlights.scm @@ -5,7 +5,8 @@ ((document) @punctuation.definition.end.comment.phpdoc.php (#set! adjust.startAndEndAroundFirstMatchOf "(?:\\*)?\\*/$")) -(tag_name) @entity.name.tag.phpdoc.php +(tag_name) @keyword.other.tag.phpdoc.php +(primitive_type) @storage.type.primitive.phpdoc.php (named_type) @storage.type.instance.phpdoc.php (variable_name) @variable.other.phpdoc.php (uri) @markup.underline.link.phpdoc.php diff --git a/packages/language-php/grammars/tree-sitter/tree-sitter-php.wasm b/packages/language-php/grammars/tree-sitter/tree-sitter-php.wasm index 759c60b33..502625bb7 100755 Binary files a/packages/language-php/grammars/tree-sitter/tree-sitter-php.wasm and b/packages/language-php/grammars/tree-sitter/tree-sitter-php.wasm differ diff --git a/packages/language-php/lib/main.js b/packages/language-php/lib/main.js index bc8b18c34..938997354 100644 --- a/packages/language-php/lib/main.js +++ b/packages/language-php/lib/main.js @@ -1,3 +1,83 @@ +const { Point, Range } = require('atom'); + +function isPhpDoc(node) { + let { text } = node; + return text.startsWith('/**') && !text.startsWith('/***') +} + +function comparePoints(a, b) { + const rows = a.row - b.row; + if (rows === 0) { + return a.column - b.column; + } else { + return rows; + } +} + +// Given a series of opening and closing PHP tags, pairs and groups them as a +// series of node specs suitable for defining the bounds of an injection. +function interpret(nodes) { + let sorted = [...nodes].sort((a, b) => { + return comparePoints(a.startPosition, b.startPosition); + }); + + let ranges = []; + let currentStart = null; + let lastIndex = nodes.length - 1; + + for (let [index, node] of sorted.entries()) { + let isStart = node.type === 'php_tag'; + let isEnd = node.type === '?>'; + let isLast = index === lastIndex; + + if (isStart) { + if (currentStart) { + throw new Error('Unbalanced content!'); + } + currentStart = node; + + if (isLast) { + // There's no ending tag to match this starting tag. This is valid and + // simply signifies that the rest of the file is PHP. We can return a + // range from here to `Infinity` and let the language mode clip it to + // the edge of the buffer. + let spec = { + startIndex: currentStart.startIndex, + startPosition: currentStart.startPosition, + endIndex: Infinity, + endPosition: Point.INFINITY, + range: new Range( + currentStart.range.start, + Point.INFINITY + ) + }; + ranges.push(spec); + currentStart = null; + break; + } + } + + if (isEnd) { + if (!currentStart) { + throw new Error('Unbalanced content!'); + } + let spec = { + startIndex: currentStart.startIndex, + startPosition: currentStart.startPosition, + endIndex: node.endIndex, + endPosition: node.endPosition, + range: new Range( + currentStart.range.start, + node.range.end + ) + }; + ranges.push(spec); + currentStart = null; + } + } + return ranges; +} + exports.activate = function () { // Here's how we handle the mixing of PHP and HTML: @@ -45,67 +125,49 @@ exports.activate = function () { return 'internal-php'; }, content(node) { - let results = []; - // At the top level we should ignore `text` nodes, since they're just - // HTML. We should also ignore the middle children of - // `text_interpolation` nodes (also `text`), but we need to include their - // first and last children, which correspond to `?>` and `']); + return interpret(boundaries); }, includeChildren: true, - newlinesBetween: true, - includeAdjacentWhitespace: true + newlinesBetween: false, + // includeAdjacentWhitespace: true, + + // For parity with the TextMate PHP grammar, we need to be able to scope + // this region with not just `source.php` but also `meta.embedded.X.php`, + // where X is one of `line` or `block` depending on whether the range spans + // multiple lines. + // + // There is no way to do this via queries because there is no discrete node + // against which we could conditionally add `meta.embedded.block` or + // `meta.embedded.line`… because of the aforementioned lunacy of the tree + // structure. + // + // So we had to invent a feature for it. When `languageScope` is a function, + // it allows the injection to decide on a range-by-range basis what the + // scope name is… _and_ it can return more than one scope name. + languageScope(grammar, _buffer, range) { + let extraScope = range.start.row !== range.end.row ? + 'meta.embedded.block.php' : 'meta.embedded.line.php'; + return [grammar.scopeName, extraScope]; + } }); - // TODOs and URLs - // ============== - - const TODO_PATTERN = /\b(TODO|FIXME|CHANGED|XXX|IDEA|HACK|NOTE|REVIEW|NB|BUG|QUESTION|COMBAK|TEMP|DEBUG|OPTIMIZE|WARNING)\b/; - const HYPERLINK_PATTERN = /\bhttps?:/ - - function isPhpDoc(node) { - let { text } = node; - return text.startsWith('/**') && !text.startsWith('/***') - } - - atom.grammars.addInjectionPoint('text.html.php', { - type: 'comment', - language: (node) => { - return TODO_PATTERN.test(node.text) ? 'todo' : undefined; - }, - content: (node) => node, - languageScope: null - }); - - for (let type of ['comment', 'string_value']) { - atom.grammars.addInjectionPoint('text.html.php', { - type, - language(node) { - // PHPDoc can parse URLs better than we can. - if (isPhpDoc(node)) return undefined; - return HYPERLINK_PATTERN.test(node.text) ? - 'hyperlink' : undefined; - }, - content: (node) => node, - languageScope: null - }); - } // HEREDOCS and NOWDOCS // ==================== @@ -141,7 +203,7 @@ exports.activate = function () { // PHPDoc // ====== - + atom.grammars.addInjectionPoint('text.html.php', { type: 'comment', language(node) { @@ -153,3 +215,19 @@ exports.activate = function () { }); }; + +// TODOs and URLs +// ============== + +exports.consumeHyperlinkInjection = (hyperlink) => { + hyperlink.addInjectionPoint('text.html.php', { + types: ['comment', 'string_value'], + language(node) { + if (isPhpDoc(node)) return null; + } + }); +}; + +exports.consumeTodoInjection = (todo) => { + todo.addInjectionPoint('text.html.php', { types: ['comment'] }); +}; diff --git a/packages/language-php/package.json b/packages/language-php/package.json index 2b3e7aa3c..011bba7ae 100644 --- a/packages/language-php/package.json +++ b/packages/language-php/package.json @@ -8,5 +8,17 @@ "node": ">=12" }, "repository": "https://github.com/pulsar-edit/pulsar", - "license": "MIT" + "license": "MIT", + "consumedServices": { + "hyperlink.injection": { + "versions": { + "0.1.0": "consumeHyperlinkInjection" + } + }, + "todo.injection": { + "versions": { + "0.1.0": "consumeTodoInjection" + } + } + } } diff --git a/packages/language-php/spec/.eslintrc.js b/packages/language-php/spec/.eslintrc.js new file mode 100644 index 000000000..77888e061 --- /dev/null +++ b/packages/language-php/spec/.eslintrc.js @@ -0,0 +1,13 @@ +module.exports = { + env: { jasmine: true }, + globals: { + waitsForPromise: true, + advanceClock: true + }, + rules: { + "node/no-unpublished-require": "off", + "node/no-extraneous-require": "off", + "no-unused-vars": "off", + "no-empty": "off" + } +}; diff --git a/packages/language-python/grammars/modern-tree-sitter-python.cson b/packages/language-python/grammars/modern-tree-sitter-python.cson index 1360c54d5..d69d15cb4 100644 --- a/packages/language-python/grammars/modern-tree-sitter-python.cson +++ b/packages/language-python/grammars/modern-tree-sitter-python.cson @@ -27,8 +27,7 @@ fileTypes: [ ] treeSitter: - # Built from tree-sitter-python 62827156d01c74dc1538266344e788da74536b8a - # to add support for `match` statements. + parserSource: 'github:tree-sitter/tree-sitter-python#4bfdd9033a2225cc95032ce77066b7aeca9e2efc' grammar: 'ts/tree-sitter-python.wasm' highlightsQuery: 'ts/highlights.scm' tagsQuery: 'ts/tags.scm' diff --git a/packages/language-python/grammars/ts/highlights.scm b/packages/language-python/grammars/ts/highlights.scm index 88874bd3b..03e8db484 100644 --- a/packages/language-python/grammars/ts/highlights.scm +++ b/packages/language-python/grammars/ts/highlights.scm @@ -187,24 +187,27 @@ ; similarly here. No need to account for the rawness of a string in the scope ; name unless someone requests that feature. +((string) @string.quoted.triple.block.format.python + (#match? @string.quoted.triple.block.format.python "^[fFrR]+\"\"\"") + (#set! capture.final)) + ((string) @string.quoted.triple.block.python (#match? @string.quoted.triple.block.python "^[bBrRuU]*\"\"\"")) -((string) @string.quoted.triple.block.format.python - (#match? @string.quoted.triple.block.format.python "^[fFrR]*\"\"\"")) +((string) @string.quoted.double.single-line.format.python + (#match? @string.quoted.double.single-line.format.python "^[fFrR]+\"") + (#set! capture.final)) ((string) @string.quoted.double.single-line.python (#match? @string.quoted.double.single-line.python "^[bBrRuU]*\"(?!\")")) -((string) @string.quoted.double.single-line.format.python - (#match? @string.quoted.double.single-line.format.python "^[fFrR]*\"")) +((string) @string.quoted.single.single-line.format.python + (#match? @string.quoted.single.single-line.format.python "^[fFrR]+?\'") + (#set! capture.final)) ((string) @string.quoted.single.single-line.python (#match? @string.quoted.single.single-line.python "^[bBrRuU]*\'")) -((string) @string.quoted.single.single-line.format.python - (#match? @string.quoted.single.single-line.format.python "^[fFrR]*?\'")) - (string_content (escape_sequence) @constant.character.escape.python) (interpolation @@ -219,7 +222,7 @@ _ @punctuation.definition.string.end.python (#is? test.last true)) -(string prefix: _ @storage.type.string.python +(string (string_start) @storage.type.string.python (#match? @storage.type.string.python "^[bBfFrRuU]+") (#set! adjust.endAfterFirstMatchOf "^[bBfFrRuU]+")) @@ -255,6 +258,7 @@ ] @keyword.control.statement._TYPE_.python [ + "break" "continue" "for" "while" @@ -393,6 +397,9 @@ "or" ] @keyword.operator.logical._TYPE_.python +; The 'not' and 'in' are each scoped separately, each one being an anonymous +; node incorrectly named "not in". +"not in" @keyword.operator.logical.not-in.python "is not" @keyword.operator.logical.is-not.python (call diff --git a/packages/language-python/grammars/ts/tree-sitter-python.wasm b/packages/language-python/grammars/ts/tree-sitter-python.wasm index 0c26bbf66..8ff8ab2a6 100755 Binary files a/packages/language-python/grammars/ts/tree-sitter-python.wasm and b/packages/language-python/grammars/ts/tree-sitter-python.wasm differ diff --git a/packages/language-python/lib/main.js b/packages/language-python/lib/main.js index 3d5a1fbed..a1f13423a 100644 --- a/packages/language-python/lib/main.js +++ b/packages/language-python/lib/main.js @@ -1,30 +1,6 @@ exports.activate = function () { if (!atom.grammars.addInjectionPoint) return; - const TODO_PATTERN = /\b(TODO|FIXME|CHANGED|XXX|IDEA|HACK|NOTE|REVIEW|NB|BUG|QUESTION|COMBAK|TEMP|DEBUG|OPTIMIZE|WARNING)\b/; - const HYPERLINK_PATTERN = /\bhttps?:/ - - atom.grammars.addInjectionPoint('source.python', { - type: 'comment', - language: (node) => { - return TODO_PATTERN.test(node.text) ? 'todo' : undefined; - }, - content: (node) => node, - languageScope: null - }); - - for (let type of ['comment', 'string']) { - atom.grammars.addInjectionPoint('source.python', { - type, - language(node) { - return HYPERLINK_PATTERN.test(node.text) ? - 'hyperlink' : undefined; - }, - content: (node) => node, - languageScope: null - }); - } - // TODO: There's no regex literal in Python. The TM-style grammar has a // very obscure option that, when enabled, assumes all raw strings are // regexes and highlights them accordingly. This might be worth doing in the @@ -42,3 +18,13 @@ exports.activate = function () { // languageScope: null // }); } + +exports.consumeHyperlinkInjection = (hyperlink) => { + hyperlink.addInjectionPoint('source.python', { + types: ['comment', 'string_content'] + }); +}; + +exports.consumeTodoInjection = (todo) => { + todo.addInjectionPoint('source.python', { types: ['comment'] }); +}; diff --git a/packages/language-python/package.json b/packages/language-python/package.json index d35fac18d..364d49faf 100644 --- a/packages/language-python/package.json +++ b/packages/language-python/package.json @@ -15,5 +15,17 @@ "dependencies": { "atom-grammar-test": "^0.6.4", "tree-sitter-python": "0.19.0" + }, + "consumedServices": { + "hyperlink.injection": { + "versions": { + "0.1.0": "consumeHyperlinkInjection" + } + }, + "todo.injection": { + "versions": { + "0.1.0": "consumeTodoInjection" + } + } } } diff --git a/packages/language-ruby/grammars/modern-tree-sitter-regex.cson b/packages/language-ruby/grammars/modern-tree-sitter-regex.cson new file mode 100644 index 000000000..f451e88a2 --- /dev/null +++ b/packages/language-ruby/grammars/modern-tree-sitter-regex.cson @@ -0,0 +1,10 @@ +scopeName: 'source.rb.regexp' +type: 'modern-tree-sitter' +parser: 'tree-sitter-regex' + +injectionRegex: '^(rb-regex)$' + +treeSitter: + parserSource: 'github:tree-sitter/tree-sitter-regex#2354482d7e2e8f8ff33c1ef6c8aa5690410fbc96' + grammar: 'tree-sitter-regex/tree-sitter-regex.wasm' + highlightsQuery: 'tree-sitter-regex/highlights.scm' diff --git a/packages/language-ruby/grammars/tree-sitter-2-ruby.cson b/packages/language-ruby/grammars/modern-tree-sitter-ruby.cson similarity index 52% rename from packages/language-ruby/grammars/tree-sitter-2-ruby.cson rename to packages/language-ruby/grammars/modern-tree-sitter-ruby.cson index ebb414df9..928fda052 100644 --- a/packages/language-ruby/grammars/tree-sitter-2-ruby.cson +++ b/packages/language-ruby/grammars/modern-tree-sitter-ruby.cson @@ -6,12 +6,13 @@ parser: 'tree-sitter-ruby' injectionRegex: 'rb|ruby|RB|RUBY' treeSitter: - grammar: 'ts/grammar.wasm' - highlightsQuery: 'ts/highlights.scm' - localsQuery: 'ts/locals.scm' - foldsQuery: 'ts/folds.scm' - indentsQuery: 'ts/indents.scm' - tagsQuery: 'ts/tags.scm' + parserSource: 'github:tree-sitter/tree-sitter-ruby#4d9ad3f010fdc47a8433adcf9ae30c8eb8475ae7' + grammar: 'tree-sitter-ruby/tree-sitter-ruby.wasm' + highlightsQuery: 'tree-sitter-ruby/highlights.scm' + localsQuery: 'tree-sitter-ruby/locals.scm' + foldsQuery: 'tree-sitter-ruby/folds.scm' + indentsQuery: 'tree-sitter-ruby/indents.scm' + tagsQuery: 'tree-sitter-ruby/tags.scm' firstLineRegex: [ # shebang line diff --git a/packages/language-ruby/grammars/tree-sitter-2-regex.cson b/packages/language-ruby/grammars/tree-sitter-2-regex.cson deleted file mode 100644 index 004b818bc..000000000 --- a/packages/language-ruby/grammars/tree-sitter-2-regex.cson +++ /dev/null @@ -1,13 +0,0 @@ -scopeName: 'source.regexp' -type: 'modern-tree-sitter' -parser: 'tree-sitter-regex' - -# TODO: Decide whether to have one regex grammar that's shared among grammars -# _or_ one separate instance of a tree-sitter-regex grammar for each language. -# In the latter case, they could still share the same `wasm` file and perhaps -# differ only in the query files. -injectionRegex: '^(rb-regex)$' - -treeSitter: - grammar: 'ts/regex/tree-sitter-regex.wasm' - highlightsQuery: 'ts/regex/highlights.scm' diff --git a/packages/language-ruby/grammars/tree-sitter-regex/highlights.scm b/packages/language-ruby/grammars/tree-sitter-regex/highlights.scm new file mode 100644 index 000000000..3f79debaf --- /dev/null +++ b/packages/language-ruby/grammars/tree-sitter-regex/highlights.scm @@ -0,0 +1,50 @@ +(non_capturing_group) @meta.group.non-capturing.regexp + +[ + (anonymous_capturing_group) +] @meta.group.capturing.regexp + +[ + (identity_escape) + (control_escape) + (character_class_escape) +] @constant.character.escape.backslash.regexp + +[ + (boundary_assertion) + (start_assertion) + (end_assertion) +] @keyword.control.anchor.regexp + +[ + (optional) + (lazy) +] @keyword.operator.quantifier.regexp + +((lookaround_assertion) @keyword.operator.lookaround.regexp + (#set! adjust.startAndEndAroundFirstMatchOf "\\?=")) + +((lookaround_assertion) @keyword.operator.lookaround.negated.regexp + (#set! adjust.startAndEndAroundFirstMatchOf "\\?!")) + +((non_capturing_group) @keyword.operator.group.non-capturing.regexp + (#set! adjust.startAndEndAroundFirstMatchOf "\\?:")) + +(anonymous_capturing_group + "(" @punctuation.definition.group.begin.bracket.round.regexp + ")" @punctuation.definition.group.end.bracket.round.regexp + (#set! capture.final true)) + +"|" @keyword.operator.or.regexp +["*" "+"] @keyword.operator.quantifier.regexp + +(character_class) @constant.other.character-class.set.regexp + +(character_class + "[" @punctuation.definition.character-class.begin.regexp) + +(character_class + "]" @punctuation.definition.character-class.end.regexp) + +(character_class + "^" @keyword.operator.negation.regexp) diff --git a/packages/language-ruby/grammars/tree-sitter-regex/tree-sitter-regex.wasm b/packages/language-ruby/grammars/tree-sitter-regex/tree-sitter-regex.wasm new file mode 100755 index 000000000..dd1988d42 Binary files /dev/null and b/packages/language-ruby/grammars/tree-sitter-regex/tree-sitter-regex.wasm differ diff --git a/packages/language-ruby/grammars/ts/folds.scm b/packages/language-ruby/grammars/tree-sitter-ruby/folds.scm similarity index 100% rename from packages/language-ruby/grammars/ts/folds.scm rename to packages/language-ruby/grammars/tree-sitter-ruby/folds.scm diff --git a/packages/language-ruby/grammars/ts/highlights.scm b/packages/language-ruby/grammars/tree-sitter-ruby/highlights.scm similarity index 100% rename from packages/language-ruby/grammars/ts/highlights.scm rename to packages/language-ruby/grammars/tree-sitter-ruby/highlights.scm diff --git a/packages/language-ruby/grammars/ts/indents.scm b/packages/language-ruby/grammars/tree-sitter-ruby/indents.scm similarity index 100% rename from packages/language-ruby/grammars/ts/indents.scm rename to packages/language-ruby/grammars/tree-sitter-ruby/indents.scm diff --git a/packages/language-ruby/grammars/ts/locals.scm b/packages/language-ruby/grammars/tree-sitter-ruby/locals.scm similarity index 100% rename from packages/language-ruby/grammars/ts/locals.scm rename to packages/language-ruby/grammars/tree-sitter-ruby/locals.scm diff --git a/packages/language-ruby/grammars/ts/tags.scm b/packages/language-ruby/grammars/tree-sitter-ruby/tags.scm similarity index 100% rename from packages/language-ruby/grammars/ts/tags.scm rename to packages/language-ruby/grammars/tree-sitter-ruby/tags.scm diff --git a/packages/language-ruby/grammars/tree-sitter-ruby/tree-sitter-ruby.wasm b/packages/language-ruby/grammars/tree-sitter-ruby/tree-sitter-ruby.wasm new file mode 100755 index 000000000..c4818c3ce Binary files /dev/null and b/packages/language-ruby/grammars/tree-sitter-ruby/tree-sitter-ruby.wasm differ diff --git a/packages/language-ruby/grammars/ts/grammar.wasm b/packages/language-ruby/grammars/ts/grammar.wasm deleted file mode 100755 index 182c005c9..000000000 Binary files a/packages/language-ruby/grammars/ts/grammar.wasm and /dev/null differ diff --git a/packages/language-ruby/grammars/ts/regex/tree-sitter-regex.wasm b/packages/language-ruby/grammars/ts/regex/tree-sitter-regex.wasm deleted file mode 100755 index 553c23b6b..000000000 Binary files a/packages/language-ruby/grammars/ts/regex/tree-sitter-regex.wasm and /dev/null differ diff --git a/packages/language-ruby/lib/main.js b/packages/language-ruby/lib/main.js index 5f343a42f..de523dfb6 100644 --- a/packages/language-ruby/lib/main.js +++ b/packages/language-ruby/lib/main.js @@ -24,33 +24,14 @@ exports.activate = function () { // coverShallowerScopes: false }); - const TODO_PATTERN = /\b(TODO|FIXME|CHANGED|XXX|IDEA|HACK|NOTE|REVIEW|NB|BUG|QUESTION|COMBAK|TEMP|DEBUG|OPTIMIZE|WARNING)\b/; - const HYPERLINK_PATTERN = /\bhttps?:/ +}; - atom.grammars.addInjectionPoint('source.ruby', { - type: 'comment', - language: (node) => { - return TODO_PATTERN.test(node.text) ? 'todo' : null; - }, - content: (node) => node, - languageScope: null - }); - - atom.grammars.addInjectionPoint('source.ruby', { - type: 'comment', - language: (node) => { - return HYPERLINK_PATTERN.test(node.text) ? 'hyperlink' : null; - }, - content: (node) => node, - languageScope: null - }); - - atom.grammars.addInjectionPoint('source.ruby', { - type: 'string_content', - language: (node) => { - return HYPERLINK_PATTERN.test(node.text) ? 'hyperlink' : null; - }, - content: (node) => node, - languageScope: null +exports.consumeHyperlinkInjection = (hyperlink) => { + hyperlink.addInjectionPoint('source.ruby', { + types: ['comment', 'string_content'] }); }; + +exports.consumeTodoInjection = (todo) => { + todo.addInjectionPoint('source.ruby', { types: ['comment'] }); +}; diff --git a/packages/language-ruby/package.json b/packages/language-ruby/package.json index 56e9c2a0d..b24fb4926 100644 --- a/packages/language-ruby/package.json +++ b/packages/language-ruby/package.json @@ -18,5 +18,17 @@ }, "devDependencies": { "dedent": "^0.7.0" + }, + "consumedServices": { + "hyperlink.injection": { + "versions": { + "0.1.0": "consumeHyperlinkInjection" + } + }, + "todo.injection": { + "versions": { + "0.1.0": "consumeTodoInjection" + } + } } } diff --git a/packages/language-rust-bundled/grammars/tree-sitter-rust/queries/highlights.scm b/packages/language-rust-bundled/grammars/tree-sitter-rust/queries/highlights.scm index 73eb3c9b5..3a74b2073 100644 --- a/packages/language-rust-bundled/grammars/tree-sitter-rust/queries/highlights.scm +++ b/packages/language-rust-bundled/grammars/tree-sitter-rust/queries/highlights.scm @@ -32,7 +32,7 @@ ; ----------- ; Wrap the "foo" and "!" of `foo!()`. -((macro_invocation) @support.other.function.rust +((macro_invocation (identifier)) @support.other.function.rust (#set! adjust.endAt firstChild.nextSibling.endPosition)) (call_expression @@ -211,6 +211,8 @@ ">>=" ] @keyword.operator.assignment.compound.rust +(reference_expression "&" @keyword.operator.reference.rust) + (binary_expression ["&" "|" "^" ">>" "<<"] @keyword.operator.bitwise.rust) diff --git a/packages/language-rust-bundled/lib/main.js b/packages/language-rust-bundled/lib/main.js index e4dc9d244..33f19c5e6 100644 --- a/packages/language-rust-bundled/lib/main.js +++ b/packages/language-rust-bundled/lib/main.js @@ -13,29 +13,21 @@ exports.activate = function () { coverShallowerScopes: true }); } - - const TODO_PATTERN = /\b(TODO|FIXME|CHANGED|XXX|IDEA|HACK|NOTE|REVIEW|NB|BUG|QUESTION|COMBAK|TEMP|DEBUG|OPTIMIZE|WARNING)\b/; - const HYPERLINK_PATTERN = /\bhttps?:/ - - for (let type of ['line_comment', 'block_comment']) { - atom.grammars.addInjectionPoint('source.rust', { - type, - language: (node) => { - return TODO_PATTERN.test(node.text) ? 'todo' : undefined; - }, - content: (node) => node, - languageScope: null - }); - } - - for (let type of ['string_literal', 'raw_string_literal', 'line_comment', 'block_comment']) { - atom.grammars.addInjectionPoint('source.rust', { - type, - language: (node) => { - return HYPERLINK_PATTERN.test(node.text) ? 'hyperlink' : undefined; - }, - content: (node) => node, - languageScope: null - }); - } +}; + +exports.consumeHyperlinkInjection = (hyperlink) => { + hyperlink.addInjectionPoint('source.rust', { + types: [ + 'line_comment', + 'block_comment', + 'string_literal', + 'raw_string_literal' + ] + }); +}; + +exports.consumeTodoInjection = (todo) => { + todo.addInjectionPoint('source.rust', { + types: ['line_comment', 'block_comment'] + }); }; diff --git a/packages/language-rust-bundled/package.json b/packages/language-rust-bundled/package.json index 603b2c602..fae238b76 100644 --- a/packages/language-rust-bundled/package.json +++ b/packages/language-rust-bundled/package.json @@ -16,5 +16,17 @@ "engines": { "atom": ">=1.0.0 <2.0.0", "node": ">=12" + }, + "consumedServices": { + "hyperlink.injection": { + "versions": { + "0.1.0": "consumeHyperlinkInjection" + } + }, + "todo.injection": { + "versions": { + "0.1.0": "consumeTodoInjection" + } + } } } diff --git a/packages/language-shellscript/grammars/tree-sitter/highlights.scm b/packages/language-shellscript/grammars/tree-sitter/highlights.scm index eaf6fcee2..f117e0065 100644 --- a/packages/language-shellscript/grammars/tree-sitter/highlights.scm +++ b/packages/language-shellscript/grammars/tree-sitter/highlights.scm @@ -33,10 +33,11 @@ "in" "do" "done" + "while" ] @keyword.control._TYPE_.shell (declaration_command - ["local" "export"] @storage.modifier._TYPE_.shell) + ["local" "export" "declare" "readonly"] @storage.modifier._TYPE_.shell) (variable_assignment (variable_name) @variable.other.member.shell @@ -90,7 +91,30 @@ (command_substitution) @meta.embedded.line.subshell.shell (#set! capture.final true)) -(command_substitution) @string.interpolation.backtick.shell +; Command substitution with backticks: var=`cmd` +((command_substitution) @string.quoted.interpolated.backtick.shell + (#match? @string.quoted.interpolated.backtick.shell "^`")) + +((command_substitution) @punctuation.definition.string.begin.shell + (#match? @punctuation.definition.string.begin.shell "^`") + (#set! adjust.endAfterFirstMatchOf "^`")) + +((command_substitution) @punctuation.definition.string.end.shell + (#match? @punctuation.definition.string.end.shell "`$") + (#set! adjust.startBeforeFirstMatchOf "`$")) + +; Command substitution of the form: var=$(cmd) +((command_substitution) @string.quoted.interpolated.dollar.shell + (#match? @string.quoted.interpolated.dollar.shell "^\\$\\(")) + +((command_substitution) @punctuation.definition.string.begin.shell + (#match? @punctuation.definition.string.begin.shell "^\\$\\(") + (#set! adjust.endAfterFirstMatchOf "^\\$\\(")) + +((command_substitution) @punctuation.definition.string.end.shell + (#match? @punctuation.definition.string.end.shell "\\)$") + (#set! adjust.startBeforeFirstMatchOf "\\)$")) + (heredoc_start) @punctuation.definition.string.begin.heredoc.shell (heredoc_body) @string.unquoted.heredoc.shell @@ -137,6 +161,7 @@ (file_redirect [ + "<" ">" ">&" "&>" @@ -154,6 +179,15 @@ (number) @constant.numeric.decimal.shell +; TODO: Double parentheses are used like `let` expressions, but ((i++)) is not +; understood by `tree-sitter-bash` as a variable increment. It needs an equals +; sign before it construes the contents as math. +(test_command + "((" @punctuation.brace.double-round.begin.shell) +(test_command + "))" @punctuation.brace.double-round.end.shell) + + (test_command "[[" @punctuation.brace.double-square.begin.shell) (test_command @@ -162,12 +196,12 @@ ; PUNCTUATION ; =========== -"{" @punctuation.brace.curly.begin.shell -"}" @punctuation.brace.curly.end.shell -"(" @punctuation.brace.round.begin.shell -")" @punctuation.brace.round.end.shell -"[" @punctuation.brace.square.begin.shell -"]" @punctuation.brace.square.end.shell +("{" @punctuation.brace.curly.begin.shell (#set! capture.shy)) +("}" @punctuation.brace.curly.end.shell (#set! capture.shy)) +("(" @punctuation.brace.round.begin.shell (#set! capture.shy)) +(")" @punctuation.brace.round.end.shell (#set! capture.shy)) +("[" @punctuation.brace.square.begin.shell (#set! capture.shy)) +("]" @punctuation.brace.square.end.shell (#set! capture.shy)) -";" @punctuation.terminator.statement.shell -":" @punctuation.separator.colon.shell +(";" @punctuation.terminator.statement.shell (#set! capture.shy)) +(":" @punctuation.separator.colon.shell (#set! capture.shy)) diff --git a/packages/language-shellscript/lib/main.js b/packages/language-shellscript/lib/main.js index e2eb7368a..706fc91f0 100644 --- a/packages/language-shellscript/lib/main.js +++ b/packages/language-shellscript/lib/main.js @@ -1,25 +1,10 @@ -exports.activate = () => { - const TODO_PATTERN = /\b(TODO|FIXME|CHANGED|XXX|IDEA|HACK|NOTE|REVIEW|NB|BUG|QUESTION|COMBAK|TEMP|DEBUG|OPTIMIZE|WARNING)\b/; - const HYPERLINK_PATTERN = /\bhttps?:/ - - atom.grammars.addInjectionPoint('source.shell', { - type: 'comment', - language: (node) => { - return TODO_PATTERN.test(node.text) ? 'todo' : undefined; - }, - content: (node) => node, - languageScope: null +exports.consumeHyperlinkInjection = (hyperlink) => { + hyperlink.addInjectionPoint('source.shell', { + types: ['comment'] }); - - atom.grammars.addInjectionPoint('source.shell', { - type: 'comment', - language(node) { - return HYPERLINK_PATTERN.test(node.text) ? - 'hyperlink' : undefined; - }, - content: (node) => node, - languageScope: null - }); - +}; + +exports.consumeTodoInjection = (todo) => { + todo.addInjectionPoint('source.shell', { types: ['comment'] }); }; diff --git a/packages/language-shellscript/package.json b/packages/language-shellscript/package.json index 47fa9bc27..0c352b7ef 100644 --- a/packages/language-shellscript/package.json +++ b/packages/language-shellscript/package.json @@ -14,5 +14,17 @@ "license": "MIT", "dependencies": { "tree-sitter-bash": "0.19.0" + }, + "consumedServices": { + "hyperlink.injection": { + "versions": { + "0.1.0": "consumeHyperlinkInjection" + } + }, + "todo.injection": { + "versions": { + "0.1.0": "consumeTodoInjection" + } + } } } diff --git a/packages/language-todo/lib/main.js b/packages/language-todo/lib/main.js new file mode 100644 index 000000000..7c20f2bb2 --- /dev/null +++ b/packages/language-todo/lib/main.js @@ -0,0 +1,65 @@ + +const TODO_PATTERN = /\b(TODO|FIXME|CHANGED|XXX|IDEA|HACK|NOTE|REVIEW|NB|BUG|QUESTION|COMBAK|TEMP|DEBUG|OPTIMIZE|WARNING)\b/; + +module.exports = { + provideTodoInjection() { + return { + // Private: Test whether a Tree-sitter node's text contains any tokens + // that would benefit from a TODO injection. + // + // Useful if you want to call {GrammarRegistry::addInjectionPoint} + // yourself and want to use this logic in a `language` callback. + // + // * `node` A Tree-sitter tree node. + test(node) { + return TODO_PATTERN.test(node.text); + }, + + // Private: specify one or more types of syntax nodes for a given grammar + // that may embed the TODO grammar. + // + // * `scopeName` The {String} ID of the parent language. + // * `options` An {Object} with the following keys: + // * `types` An {Array} or {String} indicating the type or types of + // Tree-sitter tree nodes that may receive injections. + // * `language` (optional) A {Function} that may be called to add extra + // logic for determining which language should be used in an + // injection. If present, will be called before the default logic. + // If it returns `undefined`, the default logic will apply. If it + // returns a {String} or `null`, the default logic will be preempted. + // * `content` (optional) A {Function} that will be used to determine + // which of the injection node's children, if any, will be injected + // into. The default `content` callback is one that returns the + // original node. + addInjectionPoint(scopeName, options) { + let types = options.types; + if (!Array.isArray(types)) types = [types]; + + // TODO: Ideal would be to have one `language-todo` injection for the + // whole document responsible for highlighting TODOs in all comments, + // but performance needs to be better than it is now for that to be + // possible. Injecting into individual nodes results in less time + // parsing during buffer modification, but _lots_ of language layers. + // + // Compromise is to test the content first and then only inject a layer + // for `language-todo` when we know it'll be needed. + for (let type of types) { + atom.grammars.addInjectionPoint(scopeName, { + type, + language(node) { + if (options.language) { + let result = options.language(node); + if (result !== undefined) return result; + } + return TODO_PATTERN.test(node.text) ? 'todo' : undefined; + }, + content(node) { + return options.content ? options.content(node) : node; + }, + languageScope: null + }); + } + } + }; + } +}; diff --git a/packages/language-todo/package.json b/packages/language-todo/package.json index ef6be0a45..074634e69 100644 --- a/packages/language-todo/package.json +++ b/packages/language-todo/package.json @@ -1,11 +1,19 @@ { "name": "language-todo", "version": "0.29.4", + "main": "lib/main", "description": "TODO/FIXME highlighting support in Atom", "engines": { "atom": "*", "node": ">=14" }, "repository": "https://github.com/pulsar-edit/pulsar", - "license": "MIT" + "license": "MIT", + "providedServices": { + "todo.injection": { + "versions": { + "0.1.0": "provideTodoInjection" + } + } + } } diff --git a/packages/language-toml/lib/main.js b/packages/language-toml/lib/main.js index db2280423..efe8b29c1 100644 --- a/packages/language-toml/lib/main.js +++ b/packages/language-toml/lib/main.js @@ -1,24 +1,10 @@ -exports.activate = () => { - const TODO_PATTERN = /\b(TODO|FIXME|CHANGED|XXX|IDEA|HACK|NOTE|REVIEW|NB|BUG|QUESTION|COMBAK|TEMP|DEBUG|OPTIMIZE|WARNING)\b/; - const HYPERLINK_PATTERN = /\bhttps?:/ - - atom.grammars.addInjectionPoint('source.toml', { - type: 'string', - language(node) { - return HYPERLINK_PATTERN.test(node.text) ? 'hyperlink' : undefined; - }, - content: (node) => node, - languageScope: null +exports.consumeHyperlinkInjection = (hyperlink) => { + hyperlink.addInjectionPoint('source.toml', { + types: ['comment', 'string'] }); - - atom.grammars.addInjectionPoint('source.toml', { - type: 'comment', - language(node) { - return TODO_PATTERN.test(node.text) ? 'todo' : undefined; - }, - content: (node) => node, - languageScope: null - }); - +}; + +exports.consumeTodoInjection = (todo) => { + todo.addInjectionPoint('source.toml', { types: ['comment'] }); }; diff --git a/packages/language-toml/package.json b/packages/language-toml/package.json index 0436a7eb8..2752ad583 100644 --- a/packages/language-toml/package.json +++ b/packages/language-toml/package.json @@ -11,5 +11,17 @@ }, "devDependencies": { "tree-sitter-toml": "^0.5.1" + }, + "consumedServices": { + "hyperlink.injection": { + "versions": { + "0.1.0": "consumeHyperlinkInjection" + } + }, + "todo.injection": { + "versions": { + "0.1.0": "consumeTodoInjection" + } + } } } diff --git a/packages/language-typescript/grammars/common/highlights.scm b/packages/language-typescript/grammars/common/highlights.scm index 8757d6b72..0e78ab2dd 100644 --- a/packages/language-typescript/grammars/common/highlights.scm +++ b/packages/language-typescript/grammars/common/highlights.scm @@ -9,6 +9,10 @@ (import_specifier (identifier) @variable.other.assignment.import._LANG_) +; The "*" in `import * as Foo from './bar'` +(import_clause + (namespace_import "*" @variable.other.assignment.import.all._LANG_)) + ; The "foo" in `import * as foo from './bar'` (namespace_import (identifier) @variable.other.assignment.import.namespace._LANG_) @@ -74,8 +78,6 @@ (asserts "asserts" @keyword.type.asserts._LANG_) (asserts (identifier) @variable.other.type._LANG_) -["var" "const" "let"] @storage.type._TYPE_._LANG_ - ; A simple variable declaration: ; The "foo" in `let foo = true` (variable_declarator @@ -86,6 +88,11 @@ (assignment_expression left: (identifier) @variable.other.assignment._LANG_) +; The "bar" in `foo.bar = true` +(assignment_expression + left: (member_expression + property: (property_identifier) @variable.other.assignment.property._LANG_)) + ; The "foo" in `foo += 1`. (augmented_assignment_expression left: (identifier) @variable.other.assignment._LANG_) @@ -130,18 +137,16 @@ value: (assignment_pattern left: (identifier) @variable.other.assignment.destructuring._LANG_))) -; A variable array destructuring: -; The "foo" and "bar" in `let [foo, bar] = something` -(variable_declarator - (array_pattern - (identifier) @variable.other.assignment.destructuring._LANG_)) +; An array-destructured assignment or reassignment, regardless of depth: +; The "foo" in `[foo] = bar;` and `[[foo]] = bar;`. +(array_pattern + (identifier) @variable.other.assignment.destructuring._LANG_) -; A variable array destructuring with a default: -; The "baz" in `let [foo, bar, baz = false] = something` -(variable_declarator - (array_pattern - (assignment_pattern - (identifier) @variable.other.assignment.destructuring.js))) +; An array-destructured assignment or reassignment with a default, regardless of depth: +; The "baz" in `let [foo, bar, baz = false] = something;` and `let [[baz = 5]] = something`; +(array_pattern + (assignment_pattern + (identifier) @variable.other.assignment.destructuring._LANG_)) ; A variable declaration in a for…(in|of) loop: ; The "foo" in `for (let foo of bar) {` @@ -265,6 +270,16 @@ (internal_module name: (identifier) @entity.name.type.namespace._LANG_) +; The "Bar" of `namespace Foo.Bar` and `namespace Foo.Baz.Bar`. +(internal_module + name: (nested_identifier + (identifier) @entity.name.type.namespace._LANG_ .) + (#set! isLastNamespaceSegment true)) + +; The "Foo" and "Baz" of `namespace Foo.Bar` and `namespace Foo.Baz.Bar`. +(nested_identifier (identifier) @support.type.namespace._LANG_ + (#is? test.descendantOfType "internal_module") + (#is-not? test.rangeWithData isLastNamespaceSegment)) ; DECLARATIONS ; ============ @@ -277,65 +292,273 @@ ; ========== (interface_declaration - name: (_) @entity.name.type.interface._LANG_) + name: (_) @entity.name.type.interface._LANG_ + (#set! capture.final)) ; TYPES ; ===== -["var" "let" "const"] @storage.modifier._TYPE_._LANG_ +; These go under `storage.type`/`storage.modifier` because they’re core +; language constructs. +["var" "let" "const" "class" "function"] @storage.type._TYPE_._LANG_ ["extends" "static" "async" "infer"] @storage.modifier._TYPE_._LANG_ -["class" "function"] @storage.type._TYPE_._LANG_ +(type_arguments "<" @punctuation.definition.parameters.begin.bracket.angle._LANG_ + (#set! capture.final)) +(type_arguments ">" @punctuation.definition.parameters.end.bracket.angle._LANG_ + (#set! capture.final)) + +(type_parameters "<" @punctuation.definition.parameters.begin.bracket.angle._LANG_ + (#set! capture.final)) +(type_parameters ">" @punctuation.definition.parameters.end.bracket.angle._LANG_ + (#set! capture.final)) "=>" @storage.type.arrow._LANG_ ; TODO: If I allow scopes like `storage.type.string._LANG_`, I will make a lot of ; text look like strings by accident. This really needs to be fixed in syntax ; themes. -(predefined_type _ @storage.type._LANG_ @support.type._LANG_) +; +; NOTE: To settle the long debate (in my head) about whether value types are +; `support.type` or `storage.type`, I’ve adopted the same compromised used +; by legacy Tree-sitter: value types are filed under `support.storage.type`. + +; These appear to be the primitives like `number`, `string`, `boolean`, `void`, +; et cetera. `null` and `undefined` get their own nodes. +(predefined_type _ @support.storage.type.predefined._LANG_) (type_alias_declaration name: (type_identifier) @variable.declaration.type._LANG_) -((literal_type [(null) (undefined)]) @storage.type._TEXT_._LANG_) -((literal_type [(null) (undefined)]) @support.type._TEXT_._LANG_ - (#set! capture.final true)) +((literal_type [(null) (undefined)]) @support.storage.type._TEXT_._LANG_ + (#set! capture.final)) ; TODO: Decide whether other literal types — strings, booleans, and whatnot — ; should be highlighted as they are in JS, or should be highlighted like other ; types in annotations. +; These are `storage.type` because they are core language constructs rather +; than value types. [ - "implements" "namespace" "enum" "interface" "module" "declare" +] @storage.type._TYPE_._LANG_ +"type" @storage.type._LANG_ + +; These are `storage.modifier` becase they act as adjectives and verbs for +; language constructs. +[ + "implements" "public" "private" "protected" "readonly" "satisfies" - "type" ] @storage.modifier._TYPE_._LANG_ (index_signature name: (identifier) @entity.other.attribute-name.type._LANG_) -((type_identifier) @storage.type._LANG_ - ; (#is? test.descendantOfType "type_annotation type_arguments satisfies_expression type_parameter") +; The utility types documented at +; https://www.typescriptlang.org/docs/handbook/utility-types.html. +(generic_type + (type_identifier) @support.storage.type.builtin.utility._LANG_ + (#match? @support.storage.type.builtin.utility._LANG_ "^(Awaited|Partial|Required|Readonly|Record|Pick|Omit|Exclude|Extract|NonNullable|(?:Constructor)?Parameters|(?:Return|Instance|(?:Omit)?ThisParameter|This)Type|(?:Upper|Lower)case|Capitalize|Uncapitalize)$") + (#set! capture.final)) + +; All core language builtin types. +((type_identifier) @support.storage.type.builtin._LANG_ +(#match? @support.storage.type.builtin._LANG_ "^(AggregateError|Array|ArrayBuffer|BigInt|BigInt64Array|BigUint64Array|DataView|Date|Error|EvalError|FinalizationRegistry|Float32Array|Float64Array|Function|ImageCapture|Int8Array|Int16Array|Int32Array|Map|Object|Promise|Proxy|RangeError|ReferenceError|RegExp|Set|Symbol|SyntaxError|TypeError|Uint8Array|Uint8ClampedArray|Uint16Array|Uint32Array|URIError|URL|WeakMap|WeakRef|WeakSet|XMLHttpRequest)$") + (#set! capture.final)) + +; TODO: We could add a special scope name to the entire suite of DOM types, but +; I don't have the strength for that right now. + +; +((type_identifier) @support.storage.other.type._LANG_ ) -; A capture can satisfy more than one of these criteria, so we need to guard -; against multiple matches. That's why we use `test.final` here, and why the -; two capture names are applied in separate captures — otherwise `test.final` -; would be applied after the first capture. -((type_identifier) @support.type._LANG_ - ; (#is? test.descendantOfType "type_annotation type_arguments satisfies_expression type_parameter") +; SUPPORT +; ======= + +; Array methods. +(member_expression + object: (identifier) @support.object.builtin._LANG_ + (#eq? @support.object.builtin._LANG_ "Array") + property: (property_identifier) @support.function.builtin._LANG_ + (#match? @support.function.builtin._LANG_ "^(from|isArray|of)$") + (#set! capture.final true)) + +; Date methods. +(member_expression + object: (identifier) @support.object.builtin._LANG_ + (#eq? @support.object.builtin._LANG_ "Date") + property: (property_identifier) @support.function.builtin._LANG_ + (#match? @support.function.builtin._LANG_ "^(now|parse|UTC)$") + (#set! capture.final true)) + +; JSON methods. +(member_expression + object: (identifier) @support.object.builtin._LANG_ + (#eq? @support.object.builtin._LANG_ "JSON") + property: (property_identifier) @support.function.builtin._LANG_ + (#match? @support.function.builtin._LANG_ "^(parse|stringify)$") + (#set! capture.final true)) + +; Math methods. +(member_expression + object: (identifier) @support.object.builtin._LANG_ + (#eq? @support.object.builtin._LANG_ "Math") + property: (property_identifier) @support.function.builtin._LANG_ + (#match? @support.function.builtin._LANG_ "^(abs|acos|acosh|asin|asinh|atan|atanh|atan2|cbrt|ceil|clz32|cos|cosh|exp|expm1|floor|fround|hypot|imul|log|log1p|log10|log2|max|min|pow|random|round|sign|sin|sinh|sqrt|tan|tanh|trunc)$") + (#set! capture.final true)) + +; Object methods. +(member_expression + object: (identifier) @support.object.builtin._LANG_ + (#eq? @support.object.builtin._LANG_ "Object") + property: (property_identifier) @support.function.builtin._LANG_ + (#match? @support.function.builtin._LANG_ "^(assign|create|defineProperty|defineProperties|entries|freeze|fromEntries|getOwnPropertyDescriptor|getOwnPropertyDescriptors|getOwnPropertyNames|getOwnPropertySymbols|getPrototypeOf|is|isExtensible|isFrozen|isSealed|keys|preventExtensions|seal|setPrototypeOf|values)$") + (#set! capture.final true)) + +; Reflect methods. +(member_expression + object: (identifier) @support.object.builtin._LANG_ + (#eq? @support.object.builtin._LANG_ "Reflect") + property: (property_identifier) @support.function.builtin._LANG_ + (#match? @support.function.builtin._LANG_ "^(apply|construct|defineProperty|deleteProperty|get|getOwnPropertyDescriptor|getPrototypeOf|has|isExtensible|ownKeys|preventExtensions|set|setPrototypeOf)$") + (#set! capture.final true)) + +; Intl.X instantiations. +(new_expression + constructor: (member_expression + object: (identifier) @support.object.builtin._LANG_ + (#eq? @support.object.builtin._LANG_ "Intl") + property: (property_identifier) @support.class.builtin._LANG_ + (#match? @support.class.builtin._LANG_ "^(Collator|DateTimeFormat|DisplayNames|ListFormat|Locale|NumberFormat|PluralRules|Segmenter)$")) + (#set! capture.final true)) + +; Built-in class instantiations. +(new_expression + constructor: (identifier) @support.class.builtin.instance._LANG_ + (#match? @support.class.builtin.instance._LANG_ "^(AggregateError|Array|ArrayBuffer|BigInt64Array|BigUint64Array|Boolean|DataView|Date|Error|EvalError|FinalizationRegistry|Float32Array|Float64Array|Function|ImageCapture|Int8Array|Int16Array|Int32Array|Map|Number|Object|Promise|RangeError|ReferenceError|RegExp|Set|String|SyntaxError|TypeError|Uint8Array|Uint8ClampedArray|Uint16Array|Uint32Array|URIError|URL|WeakMap|WeakRef|WeakSet|XMLHttpRequest)$") + (#set! capture.final true)) + +; Built-in constructors that can be invoked without `new`. +(call_expression + (identifier) @support.function.builtin._LANG_ + (#match? @support.function.builtin._LANG_ "^(AggregateError|Array|ArrayBuffer|Boolean|BigInt|Error|EvalError|Function|Number|Object|Proxy|RangeError|String|Symbol|SyntaxError|URIError)$") (#set! capture.final true)) +; Built-in functions. +(call_expression + (identifier) @support.function.builtin._LANG_ + (#match? @support.function.builtin._LANG_ "^(decodeURI|decodeURIComponent|encodeURI|encodeURIComponent|eval|isFinite|isNaN|parseFloat|parseInt)$") + (#set! capture.final true)) + +; Built-in `console` functions. + +(member_expression + object: (identifier) @support.class.builtin.console._LANG_ + (#eq? @support.class.builtin.console._LANG_ "console") + property: (property_identifier) @support.function.builtin.console._LANG_ + (#match? @support.function.builtin.console._LANG_ "^(assert|clear|count(Reset)?|debug|dir(xml)?|error|group(End)?info|log|profile(End)?|table|time(End|Log|Stamp)?|trace|warn)$") + (#set! capture.final true)) + +; Static methods of `Promise`. +(member_expression + object: (identifier) @support.class.builtin._LANG_ + (#eq? @support.class.builtin._LANG_ "Promise") + property: (property_identifier) @support.function.builtin._LANG_ + (#match? @support.function.builtin._LANG_ "^(all|allSettled|any|race|resolve|reject)$") + (#set! capture.final true)) + +; All “well-known” symbols (as they are referred to in the spec). +(member_expression + object: (identifier) @support.class.builtin._LANG_ + property: (property_identifier) @support.property.builtin._LANG_ + (#eq? @support.class.builtin._LANG_ "Symbol") + (#match? @support.property.builtin._LANG_ "^(asyncIterator|hasInstance|isConcatSpreadable|iterator|match|matchAll|replace|search|split|species|toPrimitive|toStringTag|unscopables)$") + (#set! capture.final true)) + +; Static methods of `Symbol`. +(member_expression + object: (identifier) @support.class.builtin._LANG_ + (#eq? @support.class.builtin._LANG_ "Symbol") + property: (property_identifier) @support.function.builtin._LANG_ + (#match? @support.function.builtin._LANG_ "^(for|keyFor)$") + (#set! capture.final true)) + +; Other built-in objects. +((identifier) @support.class.builtin._LANG_ + (#match? @support.class.builtin._LANG_ "^(Symbol)$") + (#set! capture.final true)) + +; Deprecated built-in functions. +(call_expression + (identifier) @invalid.deprecated.function._LANG_ + (#match? @invalid.deprecated.function._LANG_ "^(escape|unescape)$") + (#set! capture.final true)) + +; Built-in DOM classes. +((identifier) @support.class.builtin._LANG_ + (#match? @support.class.builtin._LANG_ "^(Document|Element|HTMLElement|HTMLDocument|HTML(Select|BR|HR|LI|Div|Map|Mod|Pre|Area|Base|Body|Data|Font|Form|Head|Html|Link|Menu|Meta|Slot|Span|Time|Audio|DList|Embed|Image|Input|Label|Media|Meter|OList|Param|Quote|Style|Table|Title|Track|UList|Video|Anchor|Button|Canvas|Dialog|IFrame|Legend|Object|Option|Output|Script|Source|Content|Details|Heading|Marquee|Picture|Unknown|DataList|FieldSet|FrameSet|MenuItem|OptGroup|Progress|TableCol|TableRow|Template|TextArea|Paragraph|TableCell|Options|TableCaption|TableSection|FormControls))$") + (#set! capture.final true)) + +; Deprecated built-in DOM classes. +((identifier) @invalid.deprecated.class._LANG_ + (#match? @invalid.deprecated.class._LANG_ "^(HTMLShadowElement)$") + (#set! capture.final true)) + +; Built-in DOM methods on `document`. +(call_expression + function: (member_expression + object: (identifier) @support.object.builtin._LANG_ + (#eq? @support.object.builtin._LANG_ "document") + property: (property_identifier) @support.function.method.builtin._LANG_ + (#match? @support.function.method.builtin._LANG_ "^(adoptNode|append|caretPositionFromPoint|caretRangeFromPoint|createAttribute(?:NS)?|createCDATASection|createComment|createDocumentFragment|createElement(?:NS)?|createEvent|createNodeIterator|createProcessingInstruction|createRange|createTextNode|createTreeWalker|elementFromPoint|elementsFromPoint|exitFullscreen|exitPictureInPicture|exitPointerLock|getAnimations|getElementById|getElementsByClassName|getElementsByTagName(?:NS)?|getSelection|hasStorageAccess|importNode|prepend|querySelector|querySelectorAll|releaseCapture|replaceChildren|requestStorageAccess|createExpression|createNSResolver|evaluate|getElementsByName|hasFocus|write|writeln|open|close)$") + (#set! capture.final true))) + +; Built-in DOM methods on nodes. These will show up as builtins on _any_ class, but +; they're distinctive enough that we're OK with that possibility. +(call_expression + function: (member_expression + property: (property_identifier) @support.function.method.builtin._LANG_ + (#match? @support.function.method.builtin._LANG_ "^(addEventListener|appendChild|cloneNode|compareDocumentPosition|contains|getElementsByClassName|getElementsByTagName(?:NS)?|getRootNode|hasChildNodes|insertBefore|isDefaultNamespace|isEqualNode|isSameNode|lookupPrefix|lookupNamespaceURI|normalize|querySelector|querySelectorAll|removeChild|replaceChild|removeEventListener)$") + (#set! capture.final true))) + +; BUILTINS +; ======== + +((identifier) @support.object.builtin._TEXT_._LANG_ + (#match? @support.object.builtin._TEXT_._LANG_ "^(arguments|module|window|document)$") + (#is-not? local) + (#set! capture.final true)) + +((identifier) @support.object.builtin.filename._LANG_ + (#eq? @support.object.builtin.filename._LANG_ "__filename") + (#is-not? local) + (#set! capture.final true)) + +((identifier) @support.object.builtin.dirname._LANG_ + (#eq? @support.object.builtin.dirname._LANG_ "__dirname") + (#is-not? local) + (#set! capture.final true)) + +((identifier) @support.function.builtin.require._LANG_ + (#eq? @support.function.builtin.require._LANG_ "require") + (#is-not? local) + (#set! capture.final true)) + +((identifier) @constant.language.infinity._LANG_ + (#eq? @constant.language.infinity._LANG_ "Infinity") + (#set! capture.final true)) + + ; OBJECTS ; ======= @@ -353,14 +576,20 @@ (#match? @constant.other.object._LANG_ "^[_A-Z]+$") (#set! capture.final true)) + ; The "foo" in `foo.bar`. (member_expression object: (identifier) @support.other.object._LANG_) -; The "bar" in `foo.bar.baz`. +; The "bar" in `foo.bar`, `foo.bar.baz`, and `foo.bar[baz]`. (member_expression - object: (member_expression - property: (property_identifier) @support.other.object._LANG_)) + property: (property_identifier) @support.other.property._LANG_) + +; ; The "bar" in `foo.bar.baz`. +; (member_expression +; object: (member_expression +; property: (property_identifier) @support.other.object._LANG_) +; (#set! capture.final)) (method_signature (property_identifier) @entity.other.attribute-name.method._LANG_) @@ -369,13 +598,9 @@ (property_identifier) @entity.other.attribute-name._LANG_) - ; FUNCTIONS ; ========= -(method_definition - name: (property_identifier) @entity.name.function.method._LANG_) - (call_expression function: (member_expression property: (property_identifier) @support.other.function.method._LANG_)) @@ -405,6 +630,11 @@ (method_definition name: (property_identifier) @entity.name.function.method.definition._LANG_) +; Private field method definitions: +; the "#foo" in `#foo () {` (inside a class body) +(method_definition + name: (private_property_identifier) @entity.name.function.method.private.definition._LANG_) + ; Function property assignment: ; The "foo" in `thing.foo = (arg) => {}` (assignment_expression @@ -431,193 +661,25 @@ key: (property_identifier) @entity.name.function.method.definition._LANG_ value: [(function) (arrow_function)]) +; Function is `storage.type` because it's a core language construct. (function "function" @storage.type.function._LANG_) (function_declaration "function" @storage.type.function._LANG_) (generator_function "function" @storage.type.function._LANG_) (generator_function_declaration "function" @storage.type.function._LANG_) +; The `*` sigil acts as a modifier on a core language construct, hence +; `storage.modifier`. (generator_function "*" @storage.modifier.generator._LANG_) (generator_function_declaration "*" @storage.modifier.generator._LANG_) (method_definition "*" @storage.modifier.generator._LANG_) (asserts "asserts" @keyword.control.type.asserts._LANG_) -; SUPPORT -; ======= - -; Array methods. -(member_expression - object: (identifier) @support.object.builtin.js - (#eq? @support.object.builtin.js "Array") - property: (property_identifier) @support.function.builtin.js - (#match? @support.function.builtin.js "^(from|isArray|of)$") - (#set! capture.final true)) - -; Date methods. -(member_expression - object: (identifier) @support.object.builtin.js - (#eq? @support.object.builtin.js "Date") - property: (property_identifier) @support.function.builtin.js - (#match? @support.function.builtin.js "^(now|parse|UTC)$") - (#set! capture.final true)) - -; JSON methods. -(member_expression - object: (identifier) @support.object.builtin.js - (#eq? @support.object.builtin.js "JSON") - property: (property_identifier) @support.function.builtin.js - (#match? @support.function.builtin.js "^(parse|stringify)$") - (#set! capture.final true)) - -; Math methods. -(member_expression - object: (identifier) @support.object.builtin.js - (#eq? @support.object.builtin.js "Math") - property: (property_identifier) @support.function.builtin.js - (#match? @support.function.builtin.js "^(abs|acos|acosh|asin|asinh|atan|atanh|atan2|cbrt|ceil|clz32|cos|cosh|exp|expm1|floor|fround|hypot|imul|log|log1p|log10|log2|max|min|pow|random|round|sign|sin|sinh|sqrt|tan|tanh|trunc)$") - (#set! capture.final true)) - -; Object methods. -(member_expression - object: (identifier) @support.object.builtin.js - (#eq? @support.object.builtin.js "Object") - property: (property_identifier) @support.function.builtin.js - (#match? @support.function.builtin.js "^(assign|create|defineProperty|defineProperties|entries|freeze|fromEntries|getOwnPropertyDescriptor|getOwnPropertyDescriptors|getOwnPropertyNames|getOwnPropertySymbols|getPrototypeOf|is|isExtensible|isFrozen|isSealed|keys|preventExtensions|seal|setPrototypeOf|values)$") - (#set! capture.final true)) - -; Reflect methods. -(member_expression - object: (identifier) @support.object.builtin.js - (#eq? @support.object.builtin.js "Reflect") - property: (property_identifier) @support.function.builtin.js - (#match? @support.function.builtin.js "^(apply|construct|defineProperty|deleteProperty|get|getOwnPropertyDescriptor|getPrototypeOf|has|isExtensible|ownKeys|preventExtensions|set|setPrototypeOf)$") - (#set! capture.final true)) - -; Intl.X instantiations. -(new_expression - constructor: (member_expression - object: (identifier) @support.object.builtin.js - (#eq? @support.object.builtin.js "Intl") - property: (property_identifier) @support.class.builtin.js - (#match? @support.class.builtin.js "^(Collator|DateTimeFormat|DisplayNames|ListFormat|Locale|NumberFormat|PluralRules|Segmenter)$")) - (#set! capture.final true)) - -; Built-in class instantiations. -(new_expression - constructor: (identifier) @support.class.builtin.instance.js - (#match? @support.class.builtin.instance.js "^(AggregateError|Array|ArrayBuffer|BigInt64Array|BigUint64Array|Boolean|DataView|Date|Error|EvalError|FinalizationRegistry|Float32Array|Float64Array|Function|ImageCapture|Int8Array|Int16Array|Int32Array|Map|Number|Object|Promise|RangeError|ReferenceError|RegExp|Set|String|SyntaxError|TypeError|Uint8Array|Uint8ClampedArray|Uint16Array|Uint32Array|URIError|URL|WeakMap|WeakRef|WeakSet|XMLHttpRequest)$") - (#set! capture.final true)) - -; Built-in constructors that can be invoked without `new`. +; An invocation of any function. (call_expression - (identifier) @support.function.builtin.js - (#match? @support.function.builtin.js "^(AggregateError|Array|ArrayBuffer|Boolean|BigInt|Error|EvalError|Function|Number|Object|Proxy|RangeError|String|Symbol|SyntaxError|URIError)$") - (#set! capture.final true)) - -; Built-in functions. -(call_expression - (identifier) @support.function.builtin.js - (#match? @support.function.builtin.js "^(decodeURI|decodeURIComponent|encodeURI|encodeURIComponent|eval|isFinite|isNaN|parseFloat|parseInt)$") - (#set! capture.final true)) - -; Built-in `console` functions. - -(member_expression - object: (identifier) @support.class.builtin.console.js - (#eq? @support.class.builtin.console.js "console") - property: (property_identifier) @support.function.builtin.console.js - (#match? @support.function.builtin.console.js "^(assert|clear|count(Reset)?|debug|dir(xml)?|error|group(End)?info|log|profile(End)?|table|time(End|Log|Stamp)?|trace|warn)$") - (#set! capture.final true)) - -; Static methods of `Promise`. -(member_expression - object: (identifier) @support.class.builtin.js - (#eq? @support.class.builtin.js "Promise") - property: (property_identifier) @support.function.builtin.js - (#match? @support.function.builtin.js "^(all|allSettled|any|race|resolve|reject)$") - (#set! capture.final true)) - -; All “well-known” symbols (as they are referred to in the spec). -(member_expression - object: (identifier) @support.class.builtin.js - property: (property_identifier) @support.property.builtin.js - (#eq? @support.class.builtin.js "Symbol") - (#match? @support.property.builtin.js "^(asyncIterator|hasInstance|isConcatSpreadable|iterator|match|matchAll|replace|search|split|species|toPrimitive|toStringTag|unscopables)$") - (#set! capture.final true)) - -; Static methods of `Symbol`. -(member_expression - object: (identifier) @support.class.builtin.js - (#eq? @support.class.builtin.js "Symbol") - property: (property_identifier) @support.function.builtin.js - (#match? @support.function.builtin.js "^(for|keyFor)$") - (#set! capture.final true)) - -; Other built-in objects. -((identifier) @support.class.builtin.js - (#match? @support.class.builtin.js "^(Symbol)$") - (#set! capture.final true)) - -; Deprecated built-in functions. -(call_expression - (identifier) @invalid.deprecated.function.js - (#match? @invalid.deprecated.function.js "^(escape|unescape)$") - (#set! capture.final true)) - -; Built-in DOM classes. -((identifier) @support.class.builtin.js - (#match? @support.class.builtin.js "^(Document|Element|HTMLElement|HTMLDocument|HTML(Select|BR|HR|LI|Div|Map|Mod|Pre|Area|Base|Body|Data|Font|Form|Head|Html|Link|Menu|Meta|Slot|Span|Time|Audio|DList|Embed|Image|Input|Label|Media|Meter|OList|Param|Quote|Style|Table|Title|Track|UList|Video|Anchor|Button|Canvas|Dialog|IFrame|Legend|Object|Option|Output|Script|Source|Content|Details|Heading|Marquee|Picture|Unknown|DataList|FieldSet|FrameSet|MenuItem|OptGroup|Progress|TableCol|TableRow|Template|TextArea|Paragraph|TableCell|Options|TableCaption|TableSection|FormControls))$") - (#set! capture.final true)) - -; Deprecated built-in DOM classes. -((identifier) @invalid.deprecated.class.js - (#match? @invalid.deprecated.class.js "^(HTMLShadowElement)$") - (#set! capture.final true)) - -; Built-in DOM methods on `document`. -(call_expression - function: (member_expression - object: (identifier) @support.object.builtin.js - (#eq? @support.object.builtin.js "document") - property: (property_identifier) @support.function.method.builtin.js - (#match? @support.function.method.builtin.js "^(adoptNode|append|caretPositionFromPoint|caretRangeFromPoint|createAttribute(?:NS)?|createCDATASection|createComment|createDocumentFragment|createElement(?:NS)?|createEvent|createNodeIterator|createProcessingInstruction|createRange|createTextNode|createTreeWalker|elementFromPoint|elementsFromPoint|exitFullscreen|exitPictureInPicture|exitPointerLock|getAnimations|getElementById|getElementsByClassName|getElementsByTagName(?:NS)?|getSelection|hasStorageAccess|importNode|prepend|querySelector|querySelectorAll|releaseCapture|replaceChildren|requestStorageAccess|createExpression|createNSResolver|evaluate|getElementsByName|hasFocus|write|writeln|open|close)$") - (#set! capture.final true))) - -; Built-in DOM methods on nodes. These will show up as builtins on _any_ class, but -; they're distinctive enough that we're OK with that possibility. -(call_expression - function: (member_expression - property: (property_identifier) @support.function.method.builtin.js - (#match? @support.function.method.builtin.js "^(addEventListener|appendChild|cloneNode|compareDocumentPosition|contains|getElementsByClassName|getElementsByTagName(?:NS)?|getRootNode|hasChildNodes|insertBefore|isDefaultNamespace|isEqualNode|isSameNode|lookupPrefix|lookupNamespaceURI|normalize|querySelector|querySelectorAll|removeChild|replaceChild|removeEventListener)$") - (#set! capture.final true))) - -; BUILTINS -; ======== - -((identifier) @support.object.builtin._TEXT_._LANG_ - (#match? @support.object.builtin._TEXT_._LANG_ "^(arguments|module|window|document)$") - (#is-not? local) - (#set! capture.final true)) - -((identifier) @support.object.builtin.filename._LANG_ - (#eq? @support.object.builtin.filename._LANG_ "__filename") - (#is-not? local) - (#set! capture.final true)) - -((identifier) @support.object.builtin.dirname._LANG_ - (#eq? @support.object.builtin.dirname._LANG_ "__dirname") - (#is-not? local) - (#set! capture.final true)) - -((identifier) @support.function.builtin.require._LANG_ - (#eq? @support.function.builtin.require._LANG_ "require") - (#is-not? local) - (#set! capture.final true)) - -((identifier) @constant.language.infinity._LANG_ - (#eq? @constant.language.infinity._LANG_ "Infinity") - (#set! capture.final true)) + function: (identifier) @support.other.function._LANG_ + (#set! capture.shy true)) ; Things that `LOOK_LIKE_CONSTANTS`. ([(property_identifier) (identifier)] @constant.other._LANG_ @@ -733,24 +795,21 @@ "||=" ] @keyword.operator.assignment.compound._LANG_ -[ - "+" - "-" - "*" - "/" - "%" -] @keyword.operator.arithmetic._LANG_ +(binary_expression + ["+" "-" "*" "/" "%"] @keyword.operator.arithmetic._LANG_) -[ - "==" - "===" - "!=" - "!==" - ">=" - "<=" - ">" - "<" -] @keyword.operator.comparison._LANG_ +(binary_expression + [ + "==" + "===" + "!=" + "!==" + ">=" + "<=" + ">" + "<" + ] @keyword.operator.comparison._LANG_ +) ["++" "--"] @keyword.operator.increment._LANG_ @@ -783,10 +842,17 @@ (ternary_expression - ["?" ":"] @keyword.operator.ternary._LANG_) + ["?" ":"] @keyword.operator.ternary._LANG_ + (#set! capture.final)) (conditional_type - ["?" ":"] @keyword.operator.ternary._LANG_) + ["?" ":"] @keyword.operator.ternary._LANG_ + (#set! capture.final)) + +; Try to highlight `?` like an operator while the user is typing without +; waiting for its paired `:`. +("?" @keyword.operator.ternary._LANG_ + (#is? test.descendantOfType "ERROR")) ; PUNCTUATION ; =========== diff --git a/packages/language-typescript/grammars/common/indents.scm b/packages/language-typescript/grammars/common/indents.scm index 427f19f0c..20329696a 100644 --- a/packages/language-typescript/grammars/common/indents.scm +++ b/packages/language-typescript/grammars/common/indents.scm @@ -16,12 +16,19 @@ (#is? test.last true)) (#set! indent.matchIndentOf parent.startPosition)) -; 'case' and 'default' need to be indented one level more than their containing -; `switch`. TODO: Might need to make this configurable. +; By default, `case` and `default` need to be indented one level more than their containing +; `switch`. (["case" "default"] @match (#set! indent.matchIndentOf parent.parent.startPosition) - (#set! indent.offsetIndent 1)) + (#set! indent.offsetIndent 1) + (#is-not? test.config "language-typescript.indentation.alignCaseWithSwitch")) +; When this config setting is enabled, `case` and `default` need to be indented +; to match their containing `switch`. +(["case" "default"] @match + (#set! indent.matchIndentOf parent.parent.startPosition) + (#set! indent.offsetIndent 0) + (#is? test.config "language-typescript.indentation.alignCaseWithSwitch")) ; ONE-LINE CONDITIONALS ; ===================== @@ -29,10 +36,12 @@ ; An `if` statement without an opening brace should indent the next line… (if_statement condition: (parenthesized_expression ")" @indent - (#is? test.lastTextOnRow true))) + (#is? test.lastTextOnRow true) + (#is? test.config "language-typescript.indentation.indentAfterBracelessIf"))) ; (as should a braceless `else`…) ("else" @indent - (#is? test.lastTextOnRow true)) + (#is? test.lastTextOnRow true) + (#is? test.config "language-typescript.indentation.indentAfterBracelessIf")) ; …and keep that indent level if the user types a comment before the ; consequence… @@ -40,7 +49,8 @@ consequence: (empty_statement) @match (#is-not? test.startsOnSameRowAs parent.startPosition) (#set! indent.matchIndentOf parent.startPosition) - (#set! indent.offsetIndent 1)) + (#set! indent.offsetIndent 1) + (#is? test.config "language-typescript.indentation.indentAfterBracelessIf")) ; …and keep that indent level after the user starts typing… (if_statement @@ -57,7 +67,8 @@ ; of an `expression_statement`, for some reason. (#not-match? @match "^\\s*{") (#set! indent.matchIndentOf parent.startPosition) - (#set! indent.offsetIndent 1)) + (#set! indent.offsetIndent 1) + (#is? test.config "language-typescript.indentation.indentAfterBracelessIf")) ; …but dedent after exactly one statement. (if_statement @@ -72,7 +83,8 @@ ] @dedent.next ; When an opening curly brace is unpaired, it might get interpreted as part ; of an `expression_statement`, for some reason. - (#not-match? @dedent.next "^\\s*{")) + (#not-match? @dedent.next "^\\s*{") + (#is? test.config "language-typescript.indentation.indentAfterBracelessIf")) (else_clause [ @@ -83,7 +95,8 @@ (throw_statement) (debugger_statement) ] @dedent.next - (#is-not? test.startsOnSameRowAs parent.startPosition)) + (#is-not? test.startsOnSameRowAs parent.startPosition) + (#is? test.config "language-typescript.indentation.indentAfterBracelessIf")) ; HANGING INDENT ON SPLIT LINES ; ============================= @@ -92,14 +105,22 @@ ; `config` scope test. ; Any of these at the end of a line indicate the next line should be indented… -(["||" "&&" "?"] @indent +(["||" "&&"] @indent + (#is? test.config "language-typescript.indentation.addHangingIndentAfterLogicalOperators") (#is? test.lastTextOnRow true)) -; …and the line after that should be dedented. +("?" @indent + (#is? test.config "language-typescript.indentation.addHangingIndentAfterTernaryOperators") + (#is? test.lastTextOnRow true)) + +; …and the line after that should be dedented… (binary_expression ["||" "&&"] right: (_) @dedent.next - (#is-not? test.startsOnSameRowAs parent.startPosition)) + (#is? test.config "language-typescript.indentation.addHangingIndentAfterLogicalOperators") + (#is-not? test.startsOnSameRowAs parent.startPosition) + ; …unless the right side of the expression spans multiple lines. + (#is? test.endsOnSameRowAs startPosition)) ; …unless it's a ternary, in which case the dedent should wait until the ; alternative clause. @@ -110,7 +131,11 @@ ; (ternary_expression alternative: (_) @dedent.next - (#is-not? test.startsOnSameRowAs parent.startPosition)) + (#is? test.config "language-typescript.indentation.addHangingIndentAfterTernaryOperators") + (#is-not? test.startsOnSameRowAs parent.startPosition) + ; Only dedent the next line if the alternative doesn't itself span multiple + ; lines. + (#is? test.endsOnSameRowAs startPosition)) ; GENERAL @@ -120,17 +145,24 @@ (template_substitution "}" @_IGNORE_ (#set! capture.final true)) -[ - "{" - "(" - "[" -] @indent +; As strange as it may seem to make all of these basic indentation hints +; configurable, some brace styles are incompatible with some of these choices; +; see https://github.com/orgs/pulsar-edit/discussions/249. +("{" @indent + (#is? test.config "language-typescript.indentation.indentBraces")) +("}" @dedent + (#is? test.config "language-typescript.indentation.indentBraces")) + +("[" @indent + (#is? test.config "language-typescript.indentation.indentBrackets")) +("]" @dedent + (#is? test.config "language-typescript.indentation.indentBrackets")) + +("(" @indent + (#is? test.config "language-typescript.indentation.indentParentheses")) +(")" @dedent + (#is? test.config "language-typescript.indentation.indentParentheses")) -[ - "}" - ")" - "]" -] @dedent (type_parameters "<" @indent) (type_parameters ">" @dedent) diff --git a/packages/language-typescript/grammars/modern-tree-sitter-regex.cson b/packages/language-typescript/grammars/modern-tree-sitter-regex.cson new file mode 100644 index 000000000..66e8e414c --- /dev/null +++ b/packages/language-typescript/grammars/modern-tree-sitter-regex.cson @@ -0,0 +1,10 @@ +scopeName: 'source.ts.regexp' +type: 'modern-tree-sitter' +parser: 'tree-sitter-regex' + +injectionRegex: '^(ts-regex)$' + +treeSitter: + parserSource: 'github:tree-sitter/tree-sitter-regex#2354482d7e2e8f8ff33c1ef6c8aa5690410fbc96' + grammar: 'tree-sitter-regex/tree-sitter-regex.wasm' + highlightsQuery: 'tree-sitter-regex/highlights.scm' diff --git a/packages/language-javascript/grammars/ts/regex/highlights.scm b/packages/language-typescript/grammars/tree-sitter-regex/highlights.scm similarity index 81% rename from packages/language-javascript/grammars/ts/regex/highlights.scm rename to packages/language-typescript/grammars/tree-sitter-regex/highlights.scm index b0018b6c7..ce8ed0a90 100644 --- a/packages/language-javascript/grammars/ts/regex/highlights.scm +++ b/packages/language-typescript/grammars/tree-sitter-regex/highlights.scm @@ -1,7 +1,3 @@ -; CAVEATS: -; -; * No support for lookbehind as of March 2023 (waiting on -; https://github.com/tree-sitter/tree-sitter-regex/pull/15) (non_capturing_group) @meta.group.non-capturing.regexp @@ -17,6 +13,8 @@ [ (boundary_assertion) + (start_assertion) + (end_assertion) ] @keyword.control.anchor.regexp [ @@ -24,10 +22,10 @@ (lazy) ] @keyword.operator.quantifier.regexp -((lookahead_assertion) @keyword.operator.lookahead.regexp +((lookaround_assertion) @keyword.operator.lookaround.regexp (#set! adjust.startAndEndAroundFirstMatchOf "\\?=")) -((lookahead_assertion) @keyword.operator.lookahead.negated.regexp +((lookaround_assertion) @keyword.operator.lookaround.negated.regexp (#set! adjust.startAndEndAroundFirstMatchOf "\\?!")) ((non_capturing_group) @keyword.operator.group.non-capturing.regexp diff --git a/packages/language-typescript/grammars/tree-sitter-regex/tree-sitter-regex.wasm b/packages/language-typescript/grammars/tree-sitter-regex/tree-sitter-regex.wasm new file mode 100755 index 000000000..dd1988d42 Binary files /dev/null and b/packages/language-typescript/grammars/tree-sitter-regex/tree-sitter-regex.wasm differ diff --git a/packages/language-typescript/grammars/tree-sitter-tsx/highlights.scm b/packages/language-typescript/grammars/tree-sitter-tsx/highlights.scm index c8b90d23a..c3a11ab01 100644 --- a/packages/language-typescript/grammars/tree-sitter-tsx/highlights.scm +++ b/packages/language-typescript/grammars/tree-sitter-tsx/highlights.scm @@ -9,18 +9,33 @@ ; The "Foo" in ``. (jsx_opening_element - name: (identifier) @entity.name.tag.ts.tsx) + name: (identifier) @entity.name.tag.ts.tsx) @meta.tag.ts.tsx + +; The "Foo.Bar" in ``. +(jsx_opening_element + name: (nested_identifier) @entity.name.tag.ts.tsx) @meta.tag.ts.tsx ; The "Foo" in ``. (jsx_closing_element - "/" @punctuation.definition.tag.end.ts.tsx - (#set! capture.final true) - name: (identifier) @entity.name.tag.ts.tsx) + name: (identifier) @entity.name.tag.ts.tsx) @meta.tag.ts.tsx + +; The "Foo.Bar" in ``. +(jsx_closing_element + name: (nested_identifier) @entity.name.tag.ts.tsx) @meta.tag.ts.tsx ; The "bar" in ``. (jsx_attribute (property_identifier) @entity.other.attribute-name.ts.tsx) +; The empty tag used as a shorthand for a fragment: `<>`. +(jsx_fragment) @meta.tag.ts.tsx + +; The slashes in closing tags should not be interpreted as math operators. +(jsx_self_closing_element "/" @punctuation.definition.tag.end.ts.tsx + (#set! capture.final true)) +(jsx_closing_element "/" @punctuation.definition.tag.end.ts.tsx + (#set! capture.final true)) + ; All JSX expressions/interpolations within braces. ((jsx_expression) @meta.embedded.block.ts.tsx (#match? @meta.embedded.block.ts.tsx "\\n") @@ -28,6 +43,18 @@ (jsx_expression) @meta.embedded.line.ts.tsx +(jsx_opening_element + "<" @punctuation.definition.tag.begin.ts.tsx + ">" @punctuation.definition.tag.end.ts.tsx) + +(jsx_closing_element + "<" @punctuation.definition.tag.begin.ts.tsx + ">" @punctuation.definition.tag.end.ts.tsx) + +(jsx_fragment + "<" @punctuation.definition.tag.begin.ts.tsx + ">" @punctuation.definition.tag.end.ts.tsx) + (jsx_self_closing_element "<" @punctuation.definition.tag.begin.ts.tsx (#set! capture.final true)) @@ -40,6 +67,7 @@ (#set! capture.final true)) + ; META ; ==== diff --git a/packages/language-typescript/lib/main.js b/packages/language-typescript/lib/main.js index dd7f40051..339496549 100644 --- a/packages/language-typescript/lib/main.js +++ b/packages/language-typescript/lib/main.js @@ -58,38 +58,31 @@ exports.activate = function () { atom.grammars.addInjectionPoint(scopeName, { type: 'regex_pattern', language() { - return 'js-regex'; + return 'ts-regex'; }, content(regex) { return regex; }, languageScope: null }); - - atom.grammars.addInjectionPoint(scopeName, { - type: 'comment', - language: (node) => { - return TODO_PATTERN.test(node.text) ? 'todo' : undefined; - }, - content: (node) => node, - languageScope: null - }); - - for (let type of ['template_string', 'string_fragment', 'comment']) { - atom.grammars.addInjectionPoint(scopeName, { - type, - language: (node) => { - return HYPERLINK_PATTERN.test(node.text) ? 'hyperlink' : undefined; - }, - content: (node) => node, - languageScope: null - }); - } } }; -const TODO_PATTERN = /\b(TODO|FIXME|CHANGED|XXX|IDEA|HACK|NOTE|REVIEW|NB|BUG|QUESTION|COMBAK|TEMP|DEBUG|OPTIMIZE|WARNING)\b/; -const HYPERLINK_PATTERN = /\bhttps?:/ +exports.consumeHyperlinkInjection = (hyperlink) => { + for (const scopeName of ['source.ts', 'source.tsx', 'source.flow']) { + hyperlink.addInjectionPoint(scopeName, { + types: ['template_string', 'string_fragment', 'comment'] + }); + } +}; + +exports.consumeTodoInjection = (todo) => { + for (const scopeName of ['source.ts', 'source.tsx', 'source.flow']) { + todo.addInjectionPoint(scopeName, { types: ['comment'] }); + } +}; + + const STYLED_REGEX = /\bstyled\b/i; function languageStringForTemplateTag(tag) { diff --git a/packages/language-typescript/package.json b/packages/language-typescript/package.json index bf01bce0f..a8b8d4365 100644 --- a/packages/language-typescript/package.json +++ b/packages/language-typescript/package.json @@ -14,5 +14,74 @@ "license": "MIT", "dependencies": { "tree-sitter-typescript": "0.20.1" + }, + "configSchema": { + "indentation": { + "title": "Indentation", + "type": "object", + "properties": { + "indentBraces": { + "title": "Indent Curly Braces", + "type": "boolean", + "default": true, + "order": 1, + "description": "Indent after `{`." + }, + "indentBrackets": { + "title": "Indent Brackets", + "type": "boolean", + "default": true, + "order": 2, + "description": "Indent after `[`." + }, + "indentParentheses": { + "title": "Indent Parentheses", + "type": "boolean", + "default": true, + "order": 3, + "description": "Indent after `(`." + }, + "alignCaseWithSwitch": { + "title": "Align “case” With ”switch”", + "type": "boolean", + "default": false, + "order": 4, + "description": "When enabled, `case` and `default` statements in `switch` blocks will match the indent level of the enclosing `switch` instead of indenting themselves one level." + }, + "indentAfterBracelessIf": { + "title": "Indent After Braceless “if” And “else”", + "type": "boolean", + "default": true, + "order": 5, + "description": "When enabled, `if` and `else` statements without a brace on the initial line will trigger an indent, then a dedent after a single statement. Disable if your brace style is incompatible with this pattern." + }, + "addHangingIndentAfterLogicalOperators": { + "title": "Add Hanging Indent After Logical Operators", + "type": "boolean", + "default": true, + "order": 6, + "description": "When enabled, will add a hanging indent when a line ends with `&&` or `||`, continuing the indent until the end of the statement." + }, + "addHangingIndentAfterTernaryOperators": { + "title": "Add Hanging Indent After Ternary Operators", + "type": "boolean", + "default": true, + "order": 7, + "description": "When enabled, will add a hanging indent when a line ends with `?`, continuing the indent through the ensuing `:` until the end of the statement." + } + } + } + }, + "consumedServices": { + "hyperlink.injection": { + "versions": { + "0.1.0": "consumeHyperlinkInjection" + } + }, + "todo.injection": { + "versions": { + "0.1.0": "consumeTodoInjection" + } + } } } diff --git a/packages/language-typescript/settings/TypeScriptReact.cson b/packages/language-typescript/settings/TypeScriptReact.cson index 4ffd62638..b99255b1f 100644 --- a/packages/language-typescript/settings/TypeScriptReact.cson +++ b/packages/language-typescript/settings/TypeScriptReact.cson @@ -17,3 +17,12 @@ 'commentEnd': ' */}', 'increaseIndentPattern': "{[^}\"']*$|\\[[^\\]\"']*$|\\([^)\"']*$|<[a-zA-Z][^/]*$|^\\s*>$", 'decreaseIndentPattern': "^\\s*(\\s*/[*].*[*]/\\s*)*[}\\])]|^\\s*()" + +'.meta.tag.tsx .meta.block.ts.tsx': + editor: + commentStart: '// ' + comments: + line: '//' + block: + start: '/*' + end: '*/' diff --git a/packages/language-yaml/grammars/tree-sitter/highlights.scm b/packages/language-yaml/grammars/tree-sitter/highlights.scm index 2f45509bb..cf7d804b6 100644 --- a/packages/language-yaml/grammars/tree-sitter/highlights.scm +++ b/packages/language-yaml/grammars/tree-sitter/highlights.scm @@ -33,7 +33,7 @@ ; STRINGS ; ======= -((string_scalar) @string.quoted.yaml) +((string_scalar) @string.unquoted.yaml) (single_quote_scalar) @string.quoted.single.yaml diff --git a/packages/language-yaml/lib/main.js b/packages/language-yaml/lib/main.js index deab0a708..793695407 100644 --- a/packages/language-yaml/lib/main.js +++ b/packages/language-yaml/lib/main.js @@ -1,23 +1,10 @@ -exports.activate = () => { - const HYPERLINK_PATTERN = /\bhttps?:/ - const TODO_PATTERN = /\b(TODO|FIXME|CHANGED|XXX|IDEA|HACK|NOTE|REVIEW|NB|BUG|QUESTION|COMBAK|TEMP|DEBUG|OPTIMIZE|WARNING)\b/; - atom.grammars.addInjectionPoint('source.yaml', { - type: 'comment', - language: (node) => { - return HYPERLINK_PATTERN.test(node.text) ? 'hyperlink' : undefined; - }, - content: (node) => node, - languageScope: null +exports.consumeHyperlinkInjection = (hyperlink) => { + hyperlink.addInjectionPoint('source.yaml', { + types: ['comment'] }); - - atom.grammars.addInjectionPoint('source.yaml', { - type: 'comment', - language: (node) => { - return TODO_PATTERN.test(node.text) ? 'todo' : undefined; - }, - content: (node) => node, - languageScope: null - }); - +}; + +exports.consumeTodoInjection = (todo) => { + todo.addInjectionPoint('source.yaml', { types: ['comment'] }); }; diff --git a/packages/language-yaml/package.json b/packages/language-yaml/package.json index 47957ef42..52fc83dfb 100644 --- a/packages/language-yaml/package.json +++ b/packages/language-yaml/package.json @@ -8,5 +8,17 @@ "node": ">=12" }, "repository": "https://github.com/pulsar-edit/pulsar", - "license": "MIT" + "license": "MIT", + "consumedServices": { + "hyperlink.injection": { + "versions": { + "0.1.0": "consumeHyperlinkInjection" + } + }, + "todo.injection": { + "versions": { + "0.1.0": "consumeTodoInjection" + } + } + } } diff --git a/packages/one-dark-syntax/styles/syntax-legacy/_base.less b/packages/one-dark-syntax/styles/syntax-legacy/_base.less index c21e13c5b..e71c968b2 100644 --- a/packages/one-dark-syntax/styles/syntax-legacy/_base.less +++ b/packages/one-dark-syntax/styles/syntax-legacy/_base.less @@ -82,6 +82,13 @@ } } +// Style `property` (anywhere in a scope name) like `variable` to keep +// continuity of appearance after some scope name changes in +// JavaScript/TypeScript. +.syntax--property { + color: @hue-5; +} + .syntax--variable { color: @hue-5; @@ -313,4 +320,10 @@ &.syntax--raw { color: @hue-4; } + + // Horizontal rules in GFM used to be scoped as `comment.hr`. For continuity, + // we assign the color of a comment to this new scope. + &.syntax--horizontal-rule { + color: @mono-3; + } } diff --git a/packages/one-dark-syntax/styles/syntax-legacy/json.less b/packages/one-dark-syntax/styles/syntax-legacy/json.less index b179ca91d..0969ac8c4 100644 --- a/packages/one-dark-syntax/styles/syntax-legacy/json.less +++ b/packages/one-dark-syntax/styles/syntax-legacy/json.less @@ -8,6 +8,13 @@ } } + // Color JSON keys differently from other strings. + .syntax--meta.syntax--structure.syntax--key { + .syntax--string.syntax--quoted.syntax--double { + color: @hue-5; + } + } + .syntax--meta.syntax--structure.syntax--dictionary.syntax--json, .syntax--meta.syntax--structure.syntax--array.syntax--json { & > .syntax--value.syntax--json > .syntax--string.syntax--quoted.syntax--json, & > .syntax--value.syntax--json > .syntax--string.syntax--quoted.syntax--json > .syntax--punctuation { diff --git a/packages/one-light-syntax/styles/syntax-legacy/_base.less b/packages/one-light-syntax/styles/syntax-legacy/_base.less index ac2a34a90..7b3f887c8 100644 --- a/packages/one-light-syntax/styles/syntax-legacy/_base.less +++ b/packages/one-light-syntax/styles/syntax-legacy/_base.less @@ -82,6 +82,13 @@ } } +// Style `property` (anywhere in a scope name) like `variable` to keep +// continuity of appearance after some scope name changes in +// JavaScript/TypeScript. +.syntax--property { + color: @hue-5; +} + .syntax--variable { color: @hue-5; @@ -313,4 +320,10 @@ &.syntax--raw { color: @hue-4; } + + // Horizontal rules in GFM used to be scoped as `comment.hr`. For continuity, + // we assign the color of a comment to this new scope. + &.syntax--horizontal-rule { + color: @mono-3; + } } diff --git a/packages/one-light-syntax/styles/syntax-legacy/json.less b/packages/one-light-syntax/styles/syntax-legacy/json.less index 6bbce0639..1b4105251 100644 --- a/packages/one-light-syntax/styles/syntax-legacy/json.less +++ b/packages/one-light-syntax/styles/syntax-legacy/json.less @@ -8,6 +8,13 @@ } } + // Color JSON keys differently from other strings. + .syntax--meta.syntax--structure.syntax--key { + .syntax--string.syntax--quoted.syntax--double { + color: @hue-5; + } + } + .syntax--meta.syntax--structure.syntax--dictionary.syntax--json, .syntax--meta.syntax--structure.syntax--array.syntax--json { & > .syntax--value.syntax--json > .syntax--string.syntax--quoted.syntax--json, diff --git a/packages/pulsar-updater/spec/.eslintrc b/packages/pulsar-updater/spec/.eslintrc new file mode 100644 index 000000000..097feaa4c --- /dev/null +++ b/packages/pulsar-updater/spec/.eslintrc @@ -0,0 +1,9 @@ +{ + "env": { "jasmine": true }, + "rules": { + "node/no-unpublished-require": "off", + "node/no-extraneous-require": "off", + "no-unused-vars": "off", + "no-empty": "off" + } +} diff --git a/packages/solarized-dark-syntax/index.less b/packages/solarized-dark-syntax/index.less index 66afd1ccf..b1a8c3699 100644 --- a/packages/solarized-dark-syntax/index.less +++ b/packages/solarized-dark-syntax/index.less @@ -12,6 +12,7 @@ @import "styles/syntax-legacy/css.less"; // @import "styles/syntax-legacy/go.less"; @import "styles/syntax-legacy/java.less"; +@import "styles/syntax-legacy/json.less"; // @import "styles/syntax-legacy/javascript.less"; @import "styles/syntax-legacy/markdown.less"; @import "styles/syntax-legacy/markup.less"; diff --git a/packages/solarized-dark-syntax/styles/syntax-legacy/json.less b/packages/solarized-dark-syntax/styles/syntax-legacy/json.less new file mode 100644 index 000000000..cdfa507c0 --- /dev/null +++ b/packages/solarized-dark-syntax/styles/syntax-legacy/json.less @@ -0,0 +1,11 @@ + +.syntax--source.syntax--json { + + // Color JSON keys differently from other strings. + .syntax--meta.syntax--structure.syntax--key { + .syntax--string.syntax--quoted.syntax--double { + color: @blue; + } + } + +} diff --git a/packages/solarized-dark-syntax/styles/syntax-legacy/markup.less b/packages/solarized-dark-syntax/styles/syntax-legacy/markup.less index dd798c269..dc58be982 100644 --- a/packages/solarized-dark-syntax/styles/syntax-legacy/markup.less +++ b/packages/solarized-dark-syntax/styles/syntax-legacy/markup.less @@ -37,4 +37,10 @@ color: @syntax-comment-color; font-style: italic; } + + // Horizontal rules in GFM used to be scoped as `comment.hr`. For continuity, + // we assign the color of a comment to this new scope. + &.syntax--horizontal-rule { + color: @syntax-comment-color; + } } diff --git a/packages/solarized-light-syntax/index.less b/packages/solarized-light-syntax/index.less index 66afd1ccf..b1a8c3699 100644 --- a/packages/solarized-light-syntax/index.less +++ b/packages/solarized-light-syntax/index.less @@ -12,6 +12,7 @@ @import "styles/syntax-legacy/css.less"; // @import "styles/syntax-legacy/go.less"; @import "styles/syntax-legacy/java.less"; +@import "styles/syntax-legacy/json.less"; // @import "styles/syntax-legacy/javascript.less"; @import "styles/syntax-legacy/markdown.less"; @import "styles/syntax-legacy/markup.less"; diff --git a/packages/solarized-light-syntax/styles/syntax-legacy/json.less b/packages/solarized-light-syntax/styles/syntax-legacy/json.less new file mode 100644 index 000000000..cdfa507c0 --- /dev/null +++ b/packages/solarized-light-syntax/styles/syntax-legacy/json.less @@ -0,0 +1,11 @@ + +.syntax--source.syntax--json { + + // Color JSON keys differently from other strings. + .syntax--meta.syntax--structure.syntax--key { + .syntax--string.syntax--quoted.syntax--double { + color: @blue; + } + } + +} diff --git a/packages/solarized-light-syntax/styles/syntax-legacy/markup.less b/packages/solarized-light-syntax/styles/syntax-legacy/markup.less index dd798c269..dc58be982 100644 --- a/packages/solarized-light-syntax/styles/syntax-legacy/markup.less +++ b/packages/solarized-light-syntax/styles/syntax-legacy/markup.less @@ -37,4 +37,10 @@ color: @syntax-comment-color; font-style: italic; } + + // Horizontal rules in GFM used to be scoped as `comment.hr`. For continuity, + // we assign the color of a comment to this new scope. + &.syntax--horizontal-rule { + color: @syntax-comment-color; + } } diff --git a/packages/styleguide/spec/.eslintrc.js b/packages/styleguide/spec/.eslintrc.js new file mode 100644 index 000000000..5226d6921 --- /dev/null +++ b/packages/styleguide/spec/.eslintrc.js @@ -0,0 +1,9 @@ +module.exports = { + env: { jasmine: true }, + rules: { + "node/no-unpublished-require": "off", + "node/no-extraneous-require": "off", + "no-unused-vars": "off", + "no-empty": "off" + } +}; diff --git a/script/validate-wasm-grammar-prs.js b/script/validate-wasm-grammar-prs.js index 706ce1f78..ff208903b 100644 --- a/script/validate-wasm-grammar-prs.js +++ b/script/validate-wasm-grammar-prs.js @@ -62,6 +62,11 @@ if (wasmFilesChanged.length === 0) { // are also accompanied by a change in the `parserSource` key for (const wasmFile of wasmFilesChanged) { + // Ignore files that have been deleted or moved. + if (!fs.existsSync(wasmFile)) { + console.log(`Skipping file that no longer exists: ${wasmFile}`); + continue; + } const wasmPath = path.dirname(wasmFile); // Don't check the base `tree-sitter.wasm` file. diff --git a/spec/config-spec.js b/spec/config-spec.js index b6b63c65c..f292542f9 100644 --- a/spec/config-spec.js +++ b/spec/config-spec.js @@ -150,6 +150,41 @@ describe('Config', () => { ).toBe(4); }); + it("merges project-specific settings with other settings when the keypath is an object", () => { + atom.config.set('x.y', 1); + atom.config.set('x.z', "fibrinolysis"); + + atom.project.replace({ + originPath: 'TEST', + paths: atom.project.getPaths(), + config: { + "*": { + "x": { + "y": 4 + } + } + } + }); + + // Project-specific settings work fine, as the spec below shows, when + // the value being retrieved is a primitive. But until recently, Pulsar + // didn't know what to do if the value being retrieved was an object. + // + // Imagine asking for _all_ config settings. The non-project-specific + // lookup returns everything. The project-specific lookup returns only + // a few overrides. Pulsar needs to _blend_ these two objects, but was + // previously choosing the project-specific lookup just because it + // wasn't undefined. + + // Here we demonstrate that it now retrieves an object for the given + // key path at the normal location and applies a project-specific + // “patch…” + expect(atom.config.get('x')).toEqual({ y: 4, z: "fibrinolysis" }) + + // …without any general settings leaking into the project config. + expect(atom.config.projectSettings.x.z).toBeUndefined(); + }); + it("ignores the project-specific source when 'excludeSources' tells it to", () => { atom.config.set('x.y', 1); diff --git a/spec/grammar-registry-spec.js b/spec/grammar-registry-spec.js index fb5c33b42..d8e932e46 100644 --- a/spec/grammar-registry-spec.js +++ b/spec/grammar-registry-spec.js @@ -258,7 +258,7 @@ describe('GrammarRegistry', () => { // TODO: Why doesn't this path resolution work like the one above? const modernTreeSitterGrammar = grammarRegistry.loadGrammarSync( require.resolve( - '../packages/language-javascript/grammars/tree-sitter-2-javascript.cson' + '../packages/language-javascript/grammars/modern-tree-sitter-javascript.cson' ) ); expect(buffer.getLanguageMode().grammar).toBe(modernTreeSitterGrammar); @@ -285,7 +285,7 @@ describe('GrammarRegistry', () => { // TODO: Why doesn't this path resolution work like the one above? const modernTreeSitterGrammar = grammarRegistry.loadGrammarSync( require.resolve( - '../packages/language-javascript/grammars/tree-sitter-2-javascript.cson' + '../packages/language-javascript/grammars/modern-tree-sitter-javascript.cson' ) ); diff --git a/spec/scope-resolver-spec.js b/spec/scope-resolver-spec.js index a8158c31f..0aee61c60 100644 --- a/spec/scope-resolver-spec.js +++ b/spec/scope-resolver-spec.js @@ -23,12 +23,12 @@ function resolve(modulePath) { } const jsGrammarPath = resolve( - 'language-javascript/grammars/tree-sitter-2-javascript.cson' + 'language-javascript/grammars/modern-tree-sitter-javascript.cson' ); let jsConfig = CSON.readFileSync(jsGrammarPath); const jsRegexGrammarPath = resolve( - 'language-javascript/grammars/tree-sitter-2-regex.cson' + 'language-javascript/grammars/modern-tree-sitter-regex.cson' ); let jsRegexConfig = CSON.readFileSync(jsRegexGrammarPath); @@ -191,6 +191,67 @@ describe('ScopeResolver', () => { ]); }); + it('does not apply any scopes when @_IGNORE_ is used', async () => { + await grammar.setQueryForTest('highlightsQuery', ` + (lexical_declaration kind: _ @_IGNORE_ + (#match? @_IGNORE_ "const")) + (lexical_declaration kind: _ @let + (#match? @let "let")) + `); + + const languageMode = new WASMTreeSitterLanguageMode({ grammar, buffer }); + buffer.setLanguageMode(languageMode); + buffer.setText(dedent` + // this is a comment + const foo = "ahaha"; + let bar = 'troz' + `); + await languageMode.ready; + + let { scopeResolver, captures } = await getAllCaptures(grammar, languageMode); + + for (let capture of captures) { + let { node, name } = capture; + let result = scopeResolver.store(capture); + if (name === '_IGNORE_') { + expect(!!result).toBe(false); + } else { + expect(!!result).toBe(true); + } + } + }); + + it('does not apply any scopes when multiple @_IGNORE_s are used', async () => { + await grammar.setQueryForTest('highlightsQuery', ` + (variable_declarator + (identifier) @_IGNORE_.identifier + (string) @_IGNORE_.string + ) + `); + + const languageMode = new WASMTreeSitterLanguageMode({ grammar, buffer }); + buffer.setLanguageMode(languageMode); + buffer.setText(dedent` + // this is a comment + const foo = "ahaha"; + let bar = false + `); + await languageMode.ready; + + let { scopeResolver, captures } = await getAllCaptures(grammar, languageMode); + + for (let capture of captures) { + let { node, name } = capture; + let result = scopeResolver.store(capture); + if (name.startsWith('_IGNORE_')) { + expect(!!result).toBe(false); + } else { + expect(!!result).toBe(true); + } + } + }); + + describe('adjustments', () => { it('adjusts ranges with (#set! adjust.startAt)', async () => { await grammar.setQueryForTest('highlightsQuery', ` diff --git a/spec/wasm-tree-sitter-language-mode-spec.js b/spec/wasm-tree-sitter-language-mode-spec.js index f899f7586..9e97324d2 100644 --- a/spec/wasm-tree-sitter-language-mode-spec.js +++ b/spec/wasm-tree-sitter-language-mode-spec.js @@ -22,15 +22,15 @@ const pythonGrammarPath = resolve( 'language-python/grammars/modern-tree-sitter-python.cson' ); const jsGrammarPath = resolve( - 'language-javascript/grammars/tree-sitter-2-javascript.cson' + 'language-javascript/grammars/modern-tree-sitter-javascript.cson' ); const jsRegexGrammarPath = resolve( - 'language-javascript/grammars/tree-sitter-2-regex.cson' + 'language-javascript/grammars/modern-tree-sitter-regex.cson' ); const jsdocGrammarPath = resolve( - 'language-javascript/grammars/tree-sitter-2-jsdoc.cson' + 'language-javascript/grammars/modern-tree-sitter-jsdoc.cson' ); const htmlGrammarPath = resolve( 'language-html/grammars/modern-tree-sitter-html.cson' @@ -39,7 +39,7 @@ const ejsGrammarPath = resolve( 'language-html/grammars/modern-tree-sitter-ejs.cson' ); const rubyGrammarPath = resolve( - 'language-ruby/grammars/tree-sitter-2-ruby.cson' + 'language-ruby/grammars/modern-tree-sitter-ruby.cson' ); const rustGrammarPath = resolve( 'language-rust-bundled/grammars/modern-tree-sitter-rust.cson' @@ -587,9 +587,7 @@ describe('WASMTreeSitterLanguageMode', () => { ] ]); - console.log('adding: ()'); buffer.setTextInRange([[0, 3], [0, 3]], '()'); - console.log('done: ()'); expectTokensToEqual(editor, [ [ @@ -598,9 +596,7 @@ describe('WASMTreeSitterLanguageMode', () => { ] ]); - console.log('adding: new'); buffer.setTextInRange([[0, 0], [0, 0]], 'new '); - console.log('done: new'); expectTokensToEqual(editor, [ [ @@ -613,7 +609,6 @@ describe('WASMTreeSitterLanguageMode', () => { await nextHighlightingUpdate(languageMode); // await wait(0); // await languageMode.atTransactionEnd(); - console.log('proceeding!'); expectTokensToEqual(editor, [ [ @@ -1031,6 +1026,32 @@ describe('WASMTreeSitterLanguageMode', () => { ]); }); + it('handles injections with no highlights query', async () => { + jasmine.useRealClock(); + atom.grammars.addGrammar(jsGrammar); + atom.grammars.addGrammar(htmlGrammar); + htmlGrammar.highlightsQuery = false; + // Pretend this grammar doesn't have a highlights query. + spyOn(htmlGrammar, 'getQuery').andReturn(Promise.resolve(null)); + const languageMode = new WASMTreeSitterLanguageMode({ + grammar: jsGrammar, + buffer, + config: atom.config, + grammars: atom.grammars + }); + buffer.setLanguageMode(languageMode); + await languageMode.ready; + + buffer.setText('text = html`

`'); + await languageMode.atTransactionEnd(); + + // An injection should still be able to add its root scope even when + // its grammar has no `highlightsQuery`. + let descriptor = editor.scopeDescriptorForBufferPosition([0, 15]); + + expect(descriptor.getScopesArray()).toContain('text.html.basic'); + }); + it('terminates comment token at the end of an injection, so that the next injection is NOT a continuation of the comment', async () => { jasmine.useRealClock(); const ejsGrammar = new WASMTreeSitterGrammar( @@ -1407,6 +1428,73 @@ describe('WASMTreeSitterLanguageMode', () => { ).toBe(true); }); + it('allows multiple base scopes on the injected layer when `languageScope` is a function', async () => { + + let customJsConfig = { ...jsConfig }; + let customJsGrammar = new WASMTreeSitterGrammar(atom.grammars, jsGrammarPath, customJsConfig); + + await jsGrammar.setQueryForTest('highlightsQuery', ` + (comment) @comment + (property_identifier) @property + (call_expression (identifier) @function) + (template_string) @string + (template_substitution + ["\${" "}"] @interpolation) + `); + + let customHtmlConfig = { ...htmlConfig }; + let customHtmlGrammar = new WASMTreeSitterGrammar(atom.grammars, htmlGrammarPath, customHtmlConfig); + + await htmlGrammar.setQueryForTest('highlightsQuery', ` + (fragment) @html + (tag_name) @tag + (attribute_name) @attr + `); + + customHtmlGrammar.addInjectionPoint({ + ...SCRIPT_TAG_INJECTION_POINT, + languageScope: (grammar, _buffer, range) => { + return [grammar.scopeName, `meta.line${range.start.row}`]; + } + }); + + jasmine.useRealClock(); + atom.grammars.addGrammar(customJsGrammar); + atom.grammars.addGrammar(customHtmlGrammar); + buffer.setText('\n
\n
\n'); + + const languageMode = new WASMTreeSitterLanguageMode({ + grammar: customHtmlGrammar, + buffer, + config: atom.config, + grammars: atom.grammars + }); + buffer.setLanguageMode(languageMode); + await languageMode.ready; + + let descriptor = languageMode.scopeDescriptorForPosition([1, 1]); + expect( + descriptor.getScopesArray().includes('source.js') + ).toBe(true); + expect( + descriptor.getScopesArray().includes(`meta.line0`) + ).toBe(true); + expect( + descriptor.getScopesArray().includes(`meta.line5`) + ).toBe(false); + + descriptor = languageMode.scopeDescriptorForPosition([6, 1]); + expect( + descriptor.getScopesArray().includes('source.js') + ).toBe(true); + expect( + descriptor.getScopesArray().includes(`meta.line5`) + ).toBe(true); + expect( + descriptor.getScopesArray().includes(`meta.line0`) + ).toBe(false); + }); + it('notifies onDidTokenize listeners the first time all syntax highlighting is done', async () => { const promise = new Promise(resolve => { editor.onDidTokenize(event => { @@ -1799,7 +1887,7 @@ describe('WASMTreeSitterLanguageMode', () => { (#set! fold.endAt lastChild.previousSibling.endPosition)) ((jsx_self_closing_element) @fold - (#set! fold.endAt lastChild.previousSibling.startPosition)) + (#set! fold.endAt lastChild.startPosition)) `); buffer.setText(dedent` diff --git a/src/config.js b/src/config.js index 2f808abf1..fa0d407fd 100644 --- a/src/config.js +++ b/src/config.js @@ -1098,8 +1098,23 @@ class Config { !sources || sources.includes(this.projectFile) ) ) { - const projectValue = getValueAtKeyPath(this.projectSettings, keyPath); - value = projectValue === undefined ? value : projectValue; + let projectValue = getValueAtKeyPath(this.projectSettings, keyPath); + if (projectValue === undefined) { + // There is no project-specific override for this key path. `value` + // stays as `value` and we pretend this never happened. + } else if (isPlainObject(value) && isPlainObject(projectValue)) { + // This key path returned an object, so we need to merge the contents + // of the two objects into a third composite object. First we clone + // the project object so as not to modify it… + projectValue = this.deepClone(projectValue); + // …then we copy over the regular value's properties, preferring the + // project-specific value wherever there is overlap. + this.deepDefaults(projectValue, value); + value = projectValue; + } else { + // This is a single value, so we prefer the project version. + value = projectValue; + } } } diff --git a/src/grammar-registry.js b/src/grammar-registry.js index c5eeeb8d2..0a233c783 100644 --- a/src/grammar-registry.js +++ b/src/grammar-registry.js @@ -288,6 +288,8 @@ module.exports = class GrammarRegistry { if (isNewTreeSitter) { if (parserConfig === 'wasm-tree-sitter') { score += 0.1; + } else if (parserConfig === 'textmate') { + score = -1; } } else if (isOldTreeSitter) { if (parserConfig === 'node-tree-sitter') { @@ -298,6 +300,8 @@ module.exports = class GrammarRegistry { // score, but just a bit less than we'd bump it if this were a // modern Tree-sitter grammar. score += 0.09; + } else if (parserConfig === 'textmate') { + score = -1; } } diff --git a/src/scope-resolver.js b/src/scope-resolver.js index 035aaaa3c..1760e4a9d 100644 --- a/src/scope-resolver.js +++ b/src/scope-resolver.js @@ -487,12 +487,15 @@ class ScopeResolver { this.setDataForRange(range, props); } - if (name === '_IGNORE_') { + if (name === '_IGNORE_' || name.startsWith('_IGNORE_.')) { // "@_IGNORE_" is a magical variable in an SCM file that will not be // applied in the grammar, but which allows us to prevent other kinds of // scopes from matching. We purposefully allowed this syntax node to set // data for a given range, but not to apply its scope ID to any // boundaries. + // + // A query can also use multiple different @_IGNORE_-style variables by + // adding segments after the @_IGNORE_, such as @_IGNORE_.foo.bar. return false; } diff --git a/src/wasm-tree-sitter-grammar.js b/src/wasm-tree-sitter-grammar.js index 85f76b9fb..fc6c87647 100644 --- a/src/wasm-tree-sitter-grammar.js +++ b/src/wasm-tree-sitter-grammar.js @@ -138,7 +138,12 @@ module.exports = class WASMTreeSitterGrammar { async getLanguage() { await parserInitPromise; if (!this._language) { - this._language = await Parser.Language.load(this.treeSitterGrammarPath); + try { + this._language = await Parser.Language.load(this.treeSitterGrammarPath); + } catch (err) { + console.error(`Error loading grammar for ${this.scopeName}; original error follows`); + throw err; + } } if (!this._queryFilesLoaded) { @@ -148,10 +153,6 @@ module.exports = class WASMTreeSitterGrammar { } async loadQueryFiles(grammarPath, queryPaths) { - if (!('highlightsQuery' in queryPaths)) { - throw new Error(`Highlights query must be present`); - } - if (this._loadQueryFilesPromise) { return this._loadQueryFilesPromise; } diff --git a/src/wasm-tree-sitter-language-mode.js b/src/wasm-tree-sitter-language-mode.js index 008f4f944..e7e54df13 100644 --- a/src/wasm-tree-sitter-language-mode.js +++ b/src/wasm-tree-sitter-language-mode.js @@ -1138,7 +1138,7 @@ class WASMTreeSitterLanguageMode { break; } } - return Math.floor(indentLength / tabLength); + return indentLength / tabLength } // Get the suggested indentation level for an existing line in the buffer. @@ -1163,6 +1163,8 @@ class WASMTreeSitterLanguageMode { ...rawOptions }; + let originalControllingLayer = options.controllingLayer; + let comparisonRow = options.comparisonRow; if (comparisonRow === undefined) { comparisonRow = row - 1; @@ -1249,7 +1251,11 @@ class WASMTreeSitterLanguageMode { // resolvers. scopeResolver.reset(); - let indentTree = options.tree; + let indentTree = null; + if (options.tree && originalControllingLayer === controllingLayer) { + // Make sure this tree belongs to the layer we expect it to. + indentTree = options.tree; + } if (!indentTree) { if (!controllingLayer.treeIsDirty || options.forceTreeParse || !this.useAsyncParsing || !this.useAsyncIndent) { @@ -1514,7 +1520,7 @@ class WASMTreeSitterLanguageMode { } scopeResolver.reset(); - let finalIndent = comparisonRowIndent + indentDelta + dedentDelta; + let finalIndent = comparisonRowIndent + indentDelta + dedentDelta + existingIndent; // console.log('score:', comparisonRowIndent, '+', indentDelta, '-', ((dedentDelta < 0) ? -dedentDelta : dedentDelta), '=', finalIndent); return Math.max(finalIndent - existingIndent, 0); @@ -1556,9 +1562,10 @@ class WASMTreeSitterLanguageMode { // the current row. let controllingLayer = this.controllingLayerAtPoint( this.buffer.clipPosition(new Point(row - 1, Infinity)), + // This query isn't as precise as the one we end up making later, but + // that's OK. This is just a first pass. (layer) => !!layer.indentsQuery && !!layer.tree ); - if (isPastedText) { // In this mode, we're not trying to auto-indent every line; instead, // we're trying to auto-indent the _first_ line of a region of text @@ -1581,7 +1588,11 @@ class WASMTreeSitterLanguageMode { let firstLineIdealIndent = this.suggestedIndentForBufferRow( row, tabLength, - { ...options, tree } + { + ...options, + controllingLayer, + tree + } ); if (firstLineIdealIndent == null) { @@ -2571,32 +2582,36 @@ class HighlightIterator { getCloseScopeIds() { let iterator = last(this.iterators); - if (this.currentScopeIsCovered) { - // console.log( - // iterator.name, - // iterator.depth, - // 'would close', - // iterator._inspectScopes( - // iterator.getCloseScopeIds() - // ), - // 'at', - // iterator.getPosition().toString(), - // 'but scope is covered!' - // ); - } else { - // console.log( - // iterator.name, - // iterator.depth, - // 'CLOSING', - // iterator.getPosition().toString(), - // iterator._inspectScopes( - // iterator.getCloseScopeIds() - // ) - // ); - } + // if (this.currentIteratorIsCovered === true || this.currentIteratorIsCovered === 'close') { + // console.log( + // iterator.name, + // iterator.depth, + // 'would close', + // iterator._inspectScopes( + // iterator.getCloseScopeIds() + // ), + // 'at', + // iterator.getPosition().toString(), + // 'but scope is covered!' + // ); + // } else { + // console.log( + // iterator.name, + // iterator.depth, + // 'CLOSING', + // iterator.getPosition().toString(), + // iterator._inspectScopes( + // iterator.getCloseScopeIds() + // ) + // ); + // } if (iterator) { - if (this.currentScopeIsCovered) { - return iterator.getOpenScopeIds().filter(id => { + // If this iterator is covered completely, or if it's covered in a + // position that prevents us from closing scopes… + if (this.currentIteratorIsCovered === true || this.currentIteratorIsCovered === 'close') { + // …then the only closing scope we're allowed to apply is one that ends + // the base scope of an injection range. + return iterator.getCloseScopeIds().filter(id => { return iterator.languageLayer.languageScopeId === id; }); } else { @@ -2609,31 +2624,35 @@ class HighlightIterator { getOpenScopeIds() { let iterator = last(this.iterators); // let ids = iterator.getOpenScopeIds(); - if (this.currentScopeIsCovered) { - // console.log( - // iterator.name, - // iterator.depth, - // 'would open', - // iterator._inspectScopes( - // iterator.getOpenScopeIds() - // ), - // 'at', - // iterator.getPosition().toString(), - // 'but scope is covered!' - // ); - } else { - // console.log( - // iterator.name, - // iterator.depth, - // 'OPENING', - // iterator.getPosition().toString(), - // iterator._inspectScopes( - // iterator.getOpenScopeIds() - // ) - // ); - } + // if (this.currentIteratorIsCovered === true || this.currentIteratorIsCovered === 'open') { + // console.log( + // iterator.name, + // iterator.depth, + // 'would open', + // iterator._inspectScopes( + // iterator.getOpenScopeIds() + // ), + // 'at', + // iterator.getPosition().toString(), + // 'but scope is covered!' + // ); + // } else { + // console.log( + // iterator.name, + // iterator.depth, + // 'OPENING', + // iterator.getPosition().toString(), + // iterator._inspectScopes( + // iterator.getOpenScopeIds() + // ) + // ); + // } if (iterator) { - if (this.currentScopeIsCovered) { + // If this iterator is covered completely, or if it's covered in a + // position that prevents us from opening scopes… + if (this.currentIteratorIsCovered === true || this.currentIteratorIsCovered === 'open') { + // …then the only opening scope we're allowed to apply is one that ends + // the base scope of an injection range. return iterator.getOpenScopeIds().filter(id => { return iterator.languageLayer.languageScopeId === id; }); @@ -2651,20 +2670,22 @@ class HighlightIterator { if (layerCount > 1) { const rest = [...this.iterators]; const leader = rest.pop(); - let covered = rest.some(it => { - return it.coversIteratorAtPosition( - leader, - leader.getPosition() - ); - }); + let covers = false; + for (let it of rest) { + let iteratorCovers = it.coversIteratorAtPosition(leader, leader.getPosition()); + if (iteratorCovers !== false) { + covers = iteratorCovers; + break; + } + } - if (covered) { - this.currentScopeIsCovered = true; + if (covers) { + this.currentIteratorIsCovered = covers; return; } } - this.currentScopeIsCovered = false; + this.currentIteratorIsCovered = false; } logPosition() { @@ -2673,6 +2694,8 @@ class HighlightIterator { } } +const EMPTY_SCOPES = Object.freeze([]); + // Iterates through everything that a `LanguageLayer` is responsible for, // marking boundaries for scope insertion. class LayerHighlightIterator { @@ -2730,16 +2753,40 @@ class LayerHighlightIterator { // …and this iterator is deeper than the other… if (iterator.depth > this.depth) { return false; } - // …and this iterator's ranges actually include this position. + // …and one of this iterator's content ranges actually includes this + // position. (With caveats!) let ranges = this.languageLayer.getCurrentRanges(); if (ranges) { - return ranges.some(range => range.containsPoint(position)); - } + // A given layer's content ranges aren't allowed to overlap each other. + // So only a single range from this list can possibly match. + let overlappingRange = ranges.find(range => range.containsPoint(position)) + if (!overlappingRange) return false; - // TODO: Despite all this, we may want to allow parent layers to apply - // scopes at the very edges of this layer's ranges/extent; or perhaps to - // apply ending scopes at starting positions and vice-versa; or at least to - // make it a configurable behavior. + // If the current position is right in the middle of an injection's + // range, then it should cover all attempts to apply scopes. But what if + // we're on one of its edges? Since closing scopes act before opening + // scopes, + // + // * if this iterator _starts_ a range at position X, it doesn't get to + // prevent another iterator from _ending_ a scope at position X; + // * if this iterator _ends_ a range at position X, it doesn't get to + // prevent another iterator from _starting_ a scope at position X. + // + // So at a given position, `currentIteratorIsCovered` can be `true` (all + // scopes suppressed), `false` (none suppressed), `"close"` (only closing + // scopes suppressed), or `"open"` (only opening scopes suppressed). + if (overlappingRange.end.compare(position) === 0) { + // We're at the right edge of the injection range. We want to prevent + // iterators from closing scopes, but not from opening them. + return 'close'; + } else if (overlappingRange.start.compare(position) === 0) { + // We're at the left edge of the injection range. We want to prevent + // iterators from opening scopes, but not from closing them. + return 'open'; + } else { + return true; + } + } } seek(start, endRow) { @@ -2766,7 +2813,7 @@ class LayerHighlightIterator { } isAtInjectionBoundary() { - let position = Point.fromObject(this.iterator.key); + let position = Point.fromObject(this.iterator.key.position); return position.isEqual(this.start) || position.isEqual(this.end); } @@ -2778,61 +2825,38 @@ class LayerHighlightIterator { } getOpenScopeIds() { - let openScopeIds = this.iterator.value.openScopeIds; - // if (openScopeIds.length > 0) { - // console.log( - // this.name, - // this.depth, - // 'OPENING', - // this.getPosition().toString(), - // this._inspectScopes( - // this.iterator.value.openScopeIds - // ) - // ); - // } - return [...openScopeIds]; + let { key, value } = this.iterator; + return key.boundary === 'end' ? EMPTY_SCOPES : [...value.scopeIds]; } getCloseScopeIds() { - let closeScopeIds = this.iterator.value.closeScopeIds; - // if (closeScopeIds.length > 0) { - // console.log( - // this.name, - // 'CLOSING', - // this.getPosition().toString(), - // this._inspectScopes( - // this.iterator.value.closeScopeIds - // ) - // ); - // } - return [...closeScopeIds]; + let { key, value } = this.iterator; + return key.boundary === 'start' ? EMPTY_SCOPES : [...value.scopeIds]; } opensScopes() { - let scopes = this.getOpenScopeIds(); - return scopes.length > 0; + return this.iterator?.key?.boundary === 'start'; } closesScopes() { - let scopes = this.getCloseScopeIds(); - return scopes.length > 0; + return this.iterator?.key?.boundary === 'end'; } getPosition() { - return this.iterator.key || Point.INFINITY; + return this.iterator?.key?.position ?? Point.INFINITY; } logPosition() { let pos = this.getPosition(); + let { key, value } = this.iterator; let { languageMode } = this.languageLayer; + let verb = key.boundary === 'end' ? 'close' : 'open'; console.log( `[highlight] (${pos.row}, ${pos.column})`, - 'close', - this.iterator.value.closeScopeIds.map(id => languageMode.scopeNameForScopeId(id)), - 'open', - this.iterator.value.openScopeIds.map(id => languageMode.scopeNameForScopeId(id)), + verb, + value.scopeIds.map(id => languageMode.scopeNameForScopeId(id)), 'next?', this.iterator.hasNext ); @@ -2840,35 +2864,33 @@ class LayerHighlightIterator { compare(other) { // First, favor the one whose current position is earlier. - const result = comparePoints(this.iterator.key, other.iterator.key); + const result = comparePoints( + this.iterator.key.position, + other.iterator.key.position + ); if (result !== 0) { return result; } // Failing that, favor iterators that need to close scopes over those that // don't. - if (this.closesScopes() && !other.closesScopes()) { + let ourBoundary = this.iterator.key.boundary; + let theirBoundary = other.iterator.key.boundary; + let bothClosing = ourBoundary === 'end' && theirBoundary === 'end'; + + if (ourBoundary === 'end' && !bothClosing) { return -1; - } else if (other.closesScopes() && !this.closesScopes()) { + } else if (theirBoundary === 'end' && !bothClosing) { return 1; } - let bothOpening = this.opensScopes() && other.opensScopes(); - let bothClosing = this.closesScopes() && other.closesScopes(); - if (bothClosing) { // When both iterators are closing scopes, the deeper layer should act // first. return other.languageLayer.depth - this.languageLayer.depth; } else { - // When both iterators are opening scopes — or if there's a mix of - // opening and closing — the shallower layer should act first. + // When both iterators are opening scopes, the shallower layer should act + // first. return this.languageLayer.depth - other.languageLayer.depth; } - - // TODO: We need to move to a system where every point in the iterator - // _either_ closes scopes _or_ opens them, with the former visited before - // the latter. Otherwise there's no correct way to sort them when two - // different layers have the same position and both want to close _and_ - // open scopes. } moveToSuccessor() { @@ -2893,7 +2915,7 @@ class LayerHighlightIterator { if (!this.end) { return false; } let next = this.peekAtSuccessor(); - return comparePoints(next, this.end) > 0; + return comparePoints(next.position, this.end) > 0; } } @@ -2949,22 +2971,14 @@ class LanguageLayer { // off this promise. We can `await this.languageLoaded` later on. this.languageLoaded = this.grammar.getLanguage().then(language => { this.language = language; - // TODO: Currently, we require a highlights query, but we might want to - // rethink this. There are use cases for treating the root layer merely - // as a way to delegate to injections, in which case syntax highlighting - // wouldn't be needed. - return this.grammar.getQuery('highlightsQuery').then(highlightsQuery => { - this.highlightsQuery = highlightsQuery; - }).catch(() => { - throw new GrammarLoadError(grammar, 'highlightsQuery'); - }); - }).then(() => { - // All other queries are optional. Regular expression language layers, - // for instance, don't really have a need for any of these. - let otherQueries = ['foldsQuery', 'indentsQuery', 'localsQuery', 'tagsQuery']; + // All queries are optional. Regular expression language layers, for + // instance, don't really have a need for any queries other than + // `highlightsQuery`, and some kinds of layers don't even need + // `highlightsQuery`. + let queries = ['highlightsQuery', 'foldsQuery', 'indentsQuery', 'localsQuery', 'tagsQuery']; let promises = []; - for (let queryType of otherQueries) { + for (let queryType of queries) { if (grammar[queryType]) { let promise = this.grammar.getQuery(queryType).then(query => { this[queryType] = query; @@ -2979,6 +2993,9 @@ class LanguageLayer { if (err.name === 'GrammarLoadError') { console.warn(err.message); if (err.queryType === 'highlightsQuery') { + // Recover by setting an empty `highlightsQuery` so that we don't + // propagate errors. + // // TODO: Warning? grammar.highlightsQuery = grammar.setQueryForTest( 'highlightsQuery', @@ -3012,10 +3029,8 @@ class LanguageLayer { // injected. languageScope = injectionPoint.languageScope; - // The `languageScope` parameter can be a function. - if (typeof languageScope === 'function') { - languageScope = languageScope(this.grammar); - } + // The `languageScope` parameter can be a function. That means we won't + // decide now; we'll decide later on a range-by-range basis. // Honor an explicit `null`, but fall back to the default scope name // otherwise. @@ -3025,7 +3040,10 @@ class LanguageLayer { } this.languageScope = languageScope; - if (languageScope === null) { + if (languageScope === null || typeof languageScope === 'function') { + // If `languageScope` is a function, we'll still often end up with a + // `languageScopeId` (or several); we just won't be able to compute it + // ahead of time. this.languageScopeId = null; } else { this.languageScopeId = this.languageMode.idForScope(languageScope); @@ -3110,17 +3128,17 @@ class LanguageLayer { // through a `ScopeResolver`. getSyntaxBoundaries(from, to) { let { buffer } = this.languageMode; - if (!(this.language && this.tree && this.highlightsQuery)) { + if (!(this.language && this.tree)) { return [[], new OpenScopeMap()]; } from = buffer.clipPosition(Point.fromObject(from, true)); to = buffer.clipPosition(Point.fromObject(to, true)); - let boundaries = createTree(comparePoints); + let boundaries = createTree(compareBoundaries); let extent = this.getExtent(); - const captures = this.highlightsQuery.captures(this.tree.rootNode, from, to); + let captures = this.highlightsQuery?.captures(this.tree.rootNode, from, to) ?? []; this.scopeResolver.reset(); for (let capture of captures) { @@ -3142,7 +3160,7 @@ class LanguageLayer { // `allowEmpty` to force these to be considered, but for marking scopes, // there's no need for it; it'd just cause us to open and close a scope // in the same position. - if (node.text === '') { continue; } + if (node.childCount === 0 && node.text === '') { continue; } // Ask the `ScopeResolver` to process each capture in turn. Some captures // will be ignored if they fail certain tests, and some will have their @@ -3205,37 +3223,65 @@ class LanguageLayer { // let includedRanges = this.depth === 0 ? [extent] : this.getCurrentRanges(); - if (this.languageScopeId) { + let languageScopeIdForRange = () => this.languageScopeId; + if (typeof this.languageScope === 'function') { + languageScopeIdForRange = (range) => { + let scopeName = this.languageScope(this.grammar, this.languageMode.buffer, range); + if (Array.isArray(scopeName)) { + return scopeName.map(s => this.languageMode.idForScope(s)); + } else { + return this.languageMode.idForScope(scopeName); + } + }; + } + + if (this.languageScopeId || typeof this.languageScope === 'function') { for (let range of includedRanges) { // Filter out ranges that have no intersection with ours. if (range.end.isLessThanOrEqual(from)) { continue; } if (range.start.isGreaterThanOrEqual(to)) { continue; } + let languageScopeIds = languageScopeIdForRange(range); + if (!languageScopeIds) continue; + + if (!Array.isArray(languageScopeIds)) { + languageScopeIds = [languageScopeIds]; + } + if (range.start.isLessThan(from)) { // If we get this far, we know that the base language scope was open // when our range began. - alreadyOpenScopes.set(range.start, [this.languageScopeId]); + alreadyOpenScopes.set( + range.start, + languageScopeIds + ); } else { // Range start must be between `from` and `to`, or else equal `from` // exactly. - this.scopeResolver.setBoundary( - range.start, - this.languageScopeId, - 'open', - { root: true, length: Infinity } - ); + for (let id of languageScopeIds) { + this.scopeResolver.setBoundary( + range.start, + id, + 'open', + { root: true, length: Infinity } + ); + } } if (range.end.isGreaterThan(to)) { // Do nothing; we don't need to set this boundary. } else { // The range must end somewhere within our range. - this.scopeResolver.setBoundary( - range.end, - this.languageScopeId, - 'close', - { root: true, length: Infinity } - ); + // + // Close the boundaries in the opposite order of how we opened them. + for (let i = languageScopeIds.length - 1; i >= 0; i--) { + this.scopeResolver.setBoundary( + range.end, + languageScopeIds[i], + 'close', + { root: true, length: Infinity } + ); + } } } } @@ -3255,12 +3301,20 @@ class LanguageLayer { continue; } - let bundle = { - closeScopeIds: [...data.close], - openScopeIds: [...data.open] - }; + let OPEN_KEY = { position: point, boundary: 'start' }; + let CLOSE_KEY = { position: point, boundary: 'end' }; - boundaries = boundaries.insert(point, bundle); + if (data.close.length > 0) { + boundaries = boundaries.insert(CLOSE_KEY, { + scopeIds: Object.freeze(data.close) + }); + } + + if (data.open.length > 0) { + boundaries = boundaries.insert(OPEN_KEY, { + scopeIds: Object.freeze(data.open) + }); + } } return [boundaries, alreadyOpenScopes]; @@ -3747,7 +3801,7 @@ class LanguageLayer { } getOrParseTree({ force = true, anonymous = false } = {}) { - if (!this.treeIsDirty || !force) { return this.tree; } + if (this.tree && (!this.treeIsDirty || !force)) { return this.tree; } // Eventually we'll take this out, but for now it serves as an indicator of // how often we have to manually re-parse in between transactions — @@ -3821,11 +3875,11 @@ class LanguageLayer { // If the cursor is resting before column X, we want all scopes that cover // the character in column X. - let captures = this.highlightsQuery.captures( + let captures = this.highlightsQuery?.captures( this.tree.rootNode, point, { row: point.row, column: point.column + 1 } - ); + ) ?? []; let results = []; for (let capture of captures) { @@ -4110,7 +4164,7 @@ class NodeRangeSet { } } - getNodeSpec (node, getChildren) { + getNodeSpec(node, getChildren) { let { startIndex, endIndex, startPosition, endPosition, id } = node; let result = { startIndex, endIndex, startPosition, endPosition, id }; if (node.children && getChildren) { @@ -4126,8 +4180,15 @@ class NodeRangeSet { const previousRanges = this.previous?.getRanges(buffer); let result = []; - for (const node of this.nodeSpecs) { + for (let node of this.nodeSpecs) { + // An injection point isn't given the point at which the buffer ends, so + // it's free to return an `endIndex` of `Infinity` here and rely on us to + // clip it to the boundary of the buffer. + if (node.endIndex === Infinity) { + node = this._clipRange(node, buffer); + } let position = node.startPosition, index = node.startIndex; + if (node.children && !this.includeChildren) { // If `includeChildren` is `false`, we're effectively collecting all // the disjoint text nodes that are direct descendants of this node. @@ -4183,6 +4244,13 @@ class NodeRangeSet { return this._consolidateRanges(result); } + _clipRange(range, buffer) { + // Convert this range spec to an actual `Range`, clip it, then convert it + // back to a range spec with accurate `startIndex` and `endIndex` values. + let clippedRange = buffer.clipRange(rangeForNode(range)); + return rangeToTreeSitterRangeSpec(clippedRange, buffer); + } + // Combine adjacent ranges to minimize the number of boundaries. _consolidateRanges(ranges) { if (ranges.length === 1) { return ranges; }