diff --git a/languages.toml b/languages.toml index 80a7de1e..c43760c2 100644 --- a/languages.toml +++ b/languages.toml @@ -1196,7 +1196,7 @@ formatter = { command = "nixfmt" } [[grammar]] name = "nix" -source = { git = "https://github.com/nix-community/tree-sitter-nix", rev = "69fbfb02896cdd27cb7ff3cd61f7f3f6bde4f017" } +source = { git = "https://github.com/numtide/tree-sitter-nix", rev = "70f34e95e30b7ebcc40815d4385d68576c4a15cd" } [[language]] name = "ruby" diff --git a/runtime/queries/nix/folds.scm b/runtime/queries/nix/folds.scm new file mode 100644 index 00000000..42a5d291 --- /dev/null +++ b/runtime/queries/nix/folds.scm @@ -0,0 +1,30 @@ +; Fold ranges for Nix. +; +; Nix has no syntactic block construct (no `{ ... }` statement blocks) +; - every region is an expression. The folds below cover the +; expression forms that are commonly multi-line in real Nix code. +; +; Capture set matches nvim-treesitter's vendored `runtime/queries/nix/ +; folds.scm`, plus the additions noted at the bottom. +; +; Editors compute fold regions from each captured node's range. +; Single-line captures are harmless. +[ + (if_expression) + (with_expression) + (let_expression) + (function_expression) + (attrset_expression) + (rec_attrset_expression) + (list_expression) + (indented_string_expression) + (let_attrset_expression) +] @fold + +; Additions over the nvim-treesitter reference set --------------------- + +; Multi-line block / doc comments. +[ + (block_comment) + (doc_comment) +] @fold diff --git a/runtime/queries/nix/highlights.scm b/runtime/queries/nix/highlights.scm index b55bcf8c..c1898709 100644 --- a/runtime/queries/nix/highlights.scm +++ b/runtime/queries/nix/highlights.scm @@ -1,97 +1,136 @@ -(comment) @comment +; tree-sitter highlighting resolves in .scm order. For a given byte +; offset, the last matching capture wins. This file is ordered from +; generic to specific: base variables and keywords first, then more +; targeted builtin / call / member patterns that override them. -[ - ";" - "." - "," - "=" - ":" - (ellipses) -] @punctuation.delimiter +; Base ---------------------------------------------------------------- -[ - "(" - ")" - "[" - "]" - "{" - "}" -] @punctuation.bracket +; Generic identifier reference. Specific rules (builtins, calls, +; booleans, constants) later in the file override this. +(variable_expression (identifier) @variable) -"assert" @keyword.control.exception -"or" @keyword.operator -"rec" @keyword.control.repeat +; Comments ------------------------------------------------------------ -[ - "if" - "then" - "else" -] @keyword.control.conditional +(comment) @comment +(doc_comment) @comment.documentation + +; Keywords ------------------------------------------------------------ [ - "let" - "inherit" + "assert" "in" - "with" + "inherit" + "let" + "rec" + "with" ] @keyword -(variable_expression name: (identifier) @variable) - -(select_expression - attrpath: (attrpath attr: (identifier)) @variable.other.member) +[ + "if" + "then" + "else" +] @keyword.control.conditional -(apply_expression - function: [ - (variable_expression name: (identifier) @function) - (select_expression - attrpath: (attrpath - attr: (identifier) @function .))]) +"or" @keyword.operator -((identifier) @variable.builtin - (#match? @variable.builtin "^(__currentSystem|__currentTime|__nixPath|__nixVersion|__storeDir|builtins)$") - (#is-not? local)) +; Literals ------------------------------------------------------------ -((identifier) @function.builtin - (#match? @function.builtin "^(__add|__addErrorContext|__all|__any|__appendContext|__attrNames|__attrValues|__bitAnd|__bitOr|__bitXor|__catAttrs|__compareVersions|__concatLists|__concatMap|__concatStringsSep|__deepSeq|__div|__elem|__elemAt|__fetchurl|__filter|__filterSource|__findFile|__foldl'|__fromJSON|__functionArgs|__genList|__genericClosure|__getAttr|__getContext|__getEnv|__hasAttr|__hasContext|__hashFile|__hashString|__head|__intersectAttrs|__isAttrs|__isBool|__isFloat|__isFunction|__isInt|__isList|__isPath|__isString|__langVersion|__length|__lessThan|__listToAttrs|__mapAttrs|__match|__mul|__parseDrvName|__partition|__path|__pathExists|__readDir|__readFile|__replaceStrings|__seq|__sort|__split|__splitVersion|__storePath|__stringLength|__sub|__substring|__tail|__toFile|__toJSON|__toPath|__toXML|__trace|__tryEval|__typeOf|__unsafeDiscardOutputDependency|__unsafeDiscardStringContext|__unsafeGetAttrPos|__valueSize|abort|baseNameOf|derivation|derivationStrict|dirOf|fetchGit|fetchMercurial|fetchTarball|fromTOML|import|isNull|map|placeholder|removeAttrs|scopedImport|throw|toString)$") - (#is-not? local)) +(integer_expression) @constant.numeric.integer +(float_expression) @constant.numeric.float [ (string_expression) (indented_string_expression) ] @string +(escape_sequence) @constant.character.escape +(dollar_escape) @constant.character.escape + [ (path_expression) (hpath_expression) (spath_expression) ] @string.special.path -(uri_expression) @string.special.uri +(uri_expression) @string.special.url -; boolean -((identifier) @constant.builtin.boolean (#match? @constant.builtin.boolean "^(true|false)$")) @constant.builtin.boolean -; null -((identifier) @constant.builtin (#eq? @constant.builtin "null")) @constant.builtin - -(integer_expression) @constant.numeric.integer -(float_expression) @constant.numeric.float - -(escape_sequence) @constant.character.escape -(dollar_escape) @constant.character.escape +; Functions ----------------------------------------------------------- +; Parameters: `x: body` and `{ a, b }: body`. (function_expression - "@"? @punctuation.delimiter - universal: (identifier) @variable.parameter - "@"? @punctuation.delimiter -) + universal: (identifier) @variable.parameter) (formal name: (identifier) @variable.parameter "?"? @punctuation.delimiter) -(interpolation - "${" @punctuation.special - "}" @punctuation.special) @embedded +(ellipses) @variable.parameter.builtin + +; Attrset members ----------------------------------------------------- + +(binding + attrpath: (attrpath (identifier)) @variable.other.member) + +(select_expression + attrpath: (attrpath (identifier)) @variable.other.member) + +(inherit attrs: (inherited_attrs attr: (identifier) @variable.other.member)) +(inherit_from attrs: (inherited_attrs attr: (identifier) @variable.other.member)) + +; Function calls ------------------------------------------------------ +; After member rules so `lib.isBool` in apply position is @function, +; not @variable.other.member. + +(apply_expression + function: [ + (variable_expression (identifier)) @function + (select_expression + attrpath: (attrpath + attr: (identifier) @function .))]) + +(binding + attrpath: (attrpath + attr: (identifier) @function) + expression: (function_expression)) + +; Builtins ------------------------------------------------------------ + +; `builtins.*` method-style calls: highlight the attr as a builtin +; function. `builtins` itself is painted by the @constant.builtin rule +; further down. +((select_expression + expression: (variable_expression + name: (identifier) @_id) + attrpath: (attrpath + attr: (identifier) @function.builtin)) + (#eq? @_id "builtins")) + +; In apply position: `map f xs` -> `map` is a function builtin. +((apply_expression + function: (variable_expression + (identifier) @function.builtin)) + (#match? @function.builtin "^(__add|__addDrvOutputDependencies|__addErrorContext|__all|__any|__appendContext|__attrNames|__attrValues|__bitAnd|__bitOr|__bitXor|__catAttrs|__ceil|__compareVersions|__concatLists|__concatMap|__concatStringsSep|__convertHash|__deepSeq|__div|__elem|__elemAt|__fetchurl|__filter|__filterSource|__findFile|__flakeRefToString|__floor|__foldl'|__fromJSON|__functionArgs|__genList|__genericClosure|__getAttr|__getContext|__getEnv|__getFlake|__groupBy|__hasAttr|__hasContext|__hashFile|__hashString|__head|__intersectAttrs|__isAttrs|__isBool|__isFloat|__isFunction|__isInt|__isList|__isPath|__isString|__length|__lessThan|__listToAttrs|__mapAttrs|__match|__mul|__parseDrvName|__parseFlakeRef|__partition|__path|__pathExists|__readDir|__readFile|__readFileType|__replaceStrings|__seq|__sort|__split|__splitVersion|__storePath|__stringLength|__sub|__substring|__tail|__toFile|__toJSON|__toPath|__toXML|__trace|__traceVerbose|__tryEval|__typeOf|__unsafeDiscardOutputDependency|__unsafeDiscardStringContext|__unsafeGetAttrPos|__warn|__zipAttrsWith|baseNameOf|break|derivation|derivationStrict|dirOf|fetchGit|fetchMercurial|fetchTarball|fetchTree|fromTOML|isNull|map|placeholder|removeAttrs|scopedImport|toString)$")) + +; `import` behaves like a keyword. +((variable_expression (identifier) @keyword.control.import) + (#eq? @keyword.control.import "import")) + +; `abort` / `throw` are control-flow exceptions. +((variable_expression (identifier) @keyword.control.exception) + (#any-of? @keyword.control.exception "abort" "throw")) + +; Booleans / constants ------------------------------------------------ + +((variable_expression (identifier) @constant.builtin.boolean) + (#any-of? @constant.builtin.boolean "true" "false")) + +((variable_expression (identifier) @constant.builtin) + (#any-of? @constant.builtin + "builtins" "null" + "__curPos" "__currentSystem" "__currentTime" "__langVersion" + "__nixPath" "__nixVersion" "__storeDir")) + +; Operators ----------------------------------------------------------- (unary_expression operator: _ @operator) @@ -99,11 +138,32 @@ (binary_expression operator: _ @operator) -(binding - attrpath: (attrpath attr: (identifier)) @variable.other.member) +[ + "=" + "@" +] @operator -(inherit_from attrs: (inherited_attrs attr: (identifier) @variable.other.member)) -(inherited_attrs attr: (identifier) @variable) +; Punctuation --------------------------------------------------------- + +[ + ";" + "." + "," + ":" +] @punctuation.delimiter + +[ + "(" + ")" + "[" + "]" + "{" + "}" +] @punctuation.bracket + +(interpolation + "${" @punctuation.special + "}" @punctuation.special) @embedded (has_attr_expression expression: (_) diff --git a/runtime/queries/nix/indents.scm b/runtime/queries/nix/indents.scm index b06b0b52..8cf93885 100644 --- a/runtime/queries/nix/indents.scm +++ b/runtime/queries/nix/indents.scm @@ -1,22 +1,46 @@ -[ - (indented_string_expression) - (string_expression) +; Indent query for Nix. +; +; Helix uses @indent / @outdent / @align rather than nvim-treesitter's +; @indent.begin / @indent.end / @indent.branch captures. - ; these are all direct parents of (binding_set) +; Nodes that introduce an indentation level: everything that opens a +; bracketed scope, plus binding-bearing expressions, call chains, and strings. +; The closing token is picked up by @outdent below. +[ (attrset_expression) - (let_attrset_expression) (rec_attrset_expression) - (let_expression) - + (let_attrset_expression) (list_expression) (parenthesized_expression) + (formals) + (binding_set) + (let_expression) + (if_expression) + (function_expression) + (binary_expression) + (apply_expression) + (select_expression) + (interpolation) + (indented_string_expression) + (string_expression) ] @indent - -(if_expression [ "if" "then" "else" ] @align) - +; Closing brackets end the indentation introduced above. [ "}" - "]" ")" + "]" ] @outdent + +; `let ... in ...` - the `in` clause is aligned with `let`. +(let_expression + "in" @align) + +; `if ... then ... else ...` - each keyword starts a branch at the same +; outer indent level. +(if_expression + [ + "if" + "then" + "else" + ] @align) diff --git a/runtime/queries/nix/injections.scm b/runtime/queries/nix/injections.scm index f9e0267b..0829cba1 100644 --- a/runtime/queries/nix/injections.scm +++ b/runtime/queries/nix/injections.scm @@ -1,27 +1,36 @@ +; Highlight links, TODOs, and similar markers inside comments by reusing the +; dedicated comment language. ((comment) @injection.content (#set! injection.language "comment")) -; mark arbitrary languages with a comment +; Mark arbitrary languages via a preceding comment. +; Helix does not use upstream's #gsub! predicates here, so the comment text is +; passed through as-is. ((((comment) @injection.language) . (indented_string_expression (string_fragment) @injection.content)) - (#set! injection.combined)) -((binding - (comment) @injection.language - expression: (indented_string_expression (string_fragment) @injection.content)) - (#set! injection.combined)) + (#set! injection.combined)) -; Common attribute keys corresponding to Python scripts, -; such as those for NixOS VM tests in nixpkgs/nixos/tests. +; nixos testScript binding - value is Python. ((binding attrpath: (attrpath (identifier) @_path) expression: (indented_string_expression (string_fragment) @injection.content)) - (#match? @_path "(^|\\.)testScript$") + (#eq? @_path "testScript") + (#set! injection.language "python") + (#set! injection.combined)) + +; nixos testScript binding with `let ... in ''...''` - value is Python. +((binding + attrpath: (attrpath (identifier) @_path) + expression: (let_expression + body: (indented_string_expression + (string_fragment) @injection.content))) + (#eq? @_path "testScript") (#set! injection.language "python") (#set! injection.combined)) -; Common attribute keys corresponding to scripts, -; such as those of stdenv.mkDerivation. +; Common binding-name -> bash injections. +; Covers Phase/Hook/Script conventions used across nixpkgs stdenv. ((binding attrpath: (attrpath (identifier) @_path) expression: [ @@ -51,7 +60,7 @@ (#set! injection.combined)) ; builtins.fromTOML toml -; Example: https://github.com/NixOS/nix/blob/3e8cd2ffe6c2c6ed8aae7853ddcfcc6d2a49b0ce/tests/functional/lang/eval-okay-fromTOML.nix +; Example: nix/tests/functional/lang/eval-okay-fromTOML.nix ((apply_expression function: (_) @_func argument: (indented_string_expression (string_fragment) @injection.content)) @@ -59,7 +68,7 @@ (#set! injection.language "toml") (#set! injection.combined)) -; trivial-builders.nix pkgs.writeShellScript[Bin] name content +; pkgs.writeShellScript / writeShellScriptBin - 2nd argument is bash. ((apply_expression function: (apply_expression function: (_) @_func) argument: (indented_string_expression (string_fragment) @injection.content)) @@ -67,8 +76,7 @@ (#set! injection.language "bash") (#set! injection.combined)) -; trivial-builders.nix, aliases.nix -; pkgs.runCommand[[No]CC][Local] name attrs content +; pkgs.runCommand variants - 3rd positional argument is bash. (apply_expression (apply_expression function: (apply_expression @@ -78,19 +86,57 @@ (#set! injection.language "bash") (#set! injection.combined)) -; trivial-builders.nix pkgs.writeShellApplication { text = content; } +; pkgs.writeShellApplication - the `text` attribute is bash. +(apply_expression + function: ((_) @_func) + argument: (_ (_)* (_ (_)* (binding + attrpath: (attrpath (identifier) @_path) + expression: (indented_string_expression + (string_fragment) @injection.content)))) + (#match? @_func "(^|\\.)writeShellApplication$") + (#match? @_path "^text$") + (#set! injection.language "bash") + (#set! injection.combined)) + +; writeShellApplication with `text = let ... in "..."` - follow the let body. (apply_expression function: ((_) @_func) argument: (_ (_)* (_ (_)* (binding attrpath: (attrpath (identifier) @_path) - expression: (indented_string_expression - (string_fragment) @injection.content)))) + expression: (let_expression + body: (indented_string_expression + (string_fragment) @injection.content))))) (#match? @_func "(^|\\.)writeShellApplication$") (#match? @_path "^text$") (#set! injection.language "bash") (#set! injection.combined)) -; trivial-builders.nix pkgs.writeCBin name content +; lib.literalExpression / lib.literalExpressionPrefix - the string +; argument is a Nix expression shown in docs; highlight as nix. +; Uses specific node-type alternation rather than (_) to avoid +; interference with other query captures. +((apply_expression + function: [ + (variable_expression (identifier) @_func) + (select_expression attrpath: (attrpath attr: (identifier) @_func .)) + ] + argument: (indented_string_expression + (string_fragment) @injection.content)) + (#match? @_func "^literalExpression(Prefix)?$") + (#set! injection.language "nix") + (#set! injection.combined)) + +((apply_expression + function: [ + (variable_expression (identifier) @_func) + (select_expression attrpath: (attrpath attr: (identifier) @_func .)) + ] + argument: (string_expression + (string_fragment) @injection.content)) + (#match? @_func "^literalExpression(Prefix)?$") + (#set! injection.language "nix")) + +; pkgs.writeCBin name content ((apply_expression function: (apply_expression function: (_) @_func) argument: (indented_string_expression (string_fragment) @injection.content)) @@ -98,8 +144,6 @@ (#set! injection.language "c") (#set! injection.combined)) -; pkgs.writers.* usage examples: nixpkgs/pkgs/build-support/writers/test.nix - ; pkgs.writers.write{Bash,Dash}[Bin] name content ((apply_expression function: (apply_expression function: (_) @_func) @@ -116,6 +160,8 @@ (#set! injection.language "fish") (#set! injection.combined)) +; pkgs.writers.* usage examples: nixpkgs/pkgs/build-support/writers/test.nix + ; pkgs.writers.writeRust[Bin] name attrs content (apply_expression (apply_expression @@ -223,10 +269,9 @@ function: ((_) @_func))) argument: (indented_string_expression (string_fragment) @injection.content) (#match? @_func "(^|\\.)writeGuile(Bin)?$") - (#set! injection.language "scheme") ; Guile is a GNU specific implementation of scheme + (#set! injection.language "scheme") (#set! injection.combined)) - ; pkgs.writers.writeBabashka[Bin] name attrs content (apply_expression (apply_expression @@ -237,15 +282,168 @@ (#set! injection.language "clojure") (#set! injection.combined)) -; pkgs.writers.writeFSharp[Bin] name content -; No query available for f-sharp as of the time of writing -; See: https://github.com/helix-editor/helix/issues/4943 -; ((apply_expression -; function: (apply_expression function: (_) @_func) -; argument: (indented_string_expression (string_fragment) @injection.content)) -; (#match? @_func "(^|\\.)writeFSharp(Bin)?$") -; (#set! injection.language "f-sharp") -; (#set! injection.combined)) +; Filename-based injection for indented strings. +; +; Detect the language from the file extension of a preceding filename +; argument in a curried call: +; +; pkgs.writeText "index.html" '' +;
Hello
+; '' +; pkgs.writeShellScriptBin "run.sh" '' +; echo hi +; '' +; +; The pattern matches `f "name.ext" '' ... ''` for any function `f`, +; minus a small denylist of common nixpkgs idioms that take a +; filename-shaped string but are not file writers (`removeSuffix`, +; `trace`, `throw`, etc.). Outside the denylist, false positives are +; tolerated: the worst case is mis-highlighting, whereas a false +; negative means no highlighting at all. +; +; Concept harvested from nix-community/tree-sitter-nix#169 by +; @nuketownada; rewritten as a hand-maintained list rather than +; generated from a Nix derivation, with the function denylist added +; per adversarial review of #53. +((apply_expression + function: (apply_expression + function: (_) @_inner_func + argument: (string_expression (string_fragment) @_filename)) + argument: (indented_string_expression (string_fragment) @injection.content)) + (#match? @_filename "\\.(sh|bash)$") + (#not-match? @_inner_func "(^|\\.)(removeSuffix|hasSuffix|hasPrefix|removePrefix|trace|throw|warn|warnIf|abort|assertMsg|seq|deepSeq|writeShellScript|writeShellScriptBin|runCommand|runCommandLocal|runCommandCC|runCommandNoCC)$") + (#set! injection.language "bash") + (#set! injection.combined)) + +((apply_expression + function: (apply_expression + function: (_) @_inner_func + argument: (string_expression (string_fragment) @_filename)) + argument: (indented_string_expression (string_fragment) @injection.content)) + (#match? @_filename "\\.(py)$") + (#not-match? @_inner_func "(^|\\.)(removeSuffix|hasSuffix|hasPrefix|removePrefix|trace|throw|warn|warnIf|abort|assertMsg|seq|deepSeq|writeShellScript|writeShellScriptBin|runCommand|runCommandLocal|runCommandCC|runCommandNoCC)$") + (#set! injection.language "python") + (#set! injection.combined)) + +((apply_expression + function: (apply_expression + function: (_) @_inner_func + argument: (string_expression (string_fragment) @_filename)) + argument: (indented_string_expression (string_fragment) @injection.content)) + (#match? @_filename "\\.(html|htm)$") + (#not-match? @_inner_func "(^|\\.)(removeSuffix|hasSuffix|hasPrefix|removePrefix|trace|throw|warn|warnIf|abort|assertMsg|seq|deepSeq|writeShellScript|writeShellScriptBin|runCommand|runCommandLocal|runCommandCC|runCommandNoCC)$") + (#set! injection.language "html") + (#set! injection.combined)) + +((apply_expression + function: (apply_expression + function: (_) @_inner_func + argument: (string_expression (string_fragment) @_filename)) + argument: (indented_string_expression (string_fragment) @injection.content)) + (#match? @_filename "\\.(css)$") + (#not-match? @_inner_func "(^|\\.)(removeSuffix|hasSuffix|hasPrefix|removePrefix|trace|throw|warn|warnIf|abort|assertMsg|seq|deepSeq|writeShellScript|writeShellScriptBin|runCommand|runCommandLocal|runCommandCC|runCommandNoCC)$") + (#set! injection.language "css") + (#set! injection.combined)) + +((apply_expression + function: (apply_expression + function: (_) @_inner_func + argument: (string_expression (string_fragment) @_filename)) + argument: (indented_string_expression (string_fragment) @injection.content)) + (#match? @_filename "\\.(js|mjs|cjs)$") + (#not-match? @_inner_func "(^|\\.)(removeSuffix|hasSuffix|hasPrefix|removePrefix|trace|throw|warn|warnIf|abort|assertMsg|seq|deepSeq|writeShellScript|writeShellScriptBin|runCommand|runCommandLocal|runCommandCC|runCommandNoCC)$") + (#set! injection.language "javascript") + (#set! injection.combined)) + +((apply_expression + function: (apply_expression + function: (_) @_inner_func + argument: (string_expression (string_fragment) @_filename)) + argument: (indented_string_expression (string_fragment) @injection.content)) + (#match? @_filename "\\.(ts|mts|cts)$") + (#not-match? @_inner_func "(^|\\.)(removeSuffix|hasSuffix|hasPrefix|removePrefix|trace|throw|warn|warnIf|abort|assertMsg|seq|deepSeq|writeShellScript|writeShellScriptBin|runCommand|runCommandLocal|runCommandCC|runCommandNoCC)$") + (#set! injection.language "typescript") + (#set! injection.combined)) + +((apply_expression + function: (apply_expression + function: (_) @_inner_func + argument: (string_expression (string_fragment) @_filename)) + argument: (indented_string_expression (string_fragment) @injection.content)) + (#match? @_filename "\\.(json)$") + (#not-match? @_inner_func "(^|\\.)(removeSuffix|hasSuffix|hasPrefix|removePrefix|trace|throw|warn|warnIf|abort|assertMsg|seq|deepSeq|writeShellScript|writeShellScriptBin|runCommand|runCommandLocal|runCommandCC|runCommandNoCC)$") + (#set! injection.language "json") + (#set! injection.combined)) + +((apply_expression + function: (apply_expression + function: (_) @_inner_func + argument: (string_expression (string_fragment) @_filename)) + argument: (indented_string_expression (string_fragment) @injection.content)) + (#match? @_filename "\\.(yml|yaml)$") + (#not-match? @_inner_func "(^|\\.)(removeSuffix|hasSuffix|hasPrefix|removePrefix|trace|throw|warn|warnIf|abort|assertMsg|seq|deepSeq|writeShellScript|writeShellScriptBin|runCommand|runCommandLocal|runCommandCC|runCommandNoCC)$") + (#set! injection.language "yaml") + (#set! injection.combined)) + +((apply_expression + function: (apply_expression + function: (_) @_inner_func + argument: (string_expression (string_fragment) @_filename)) + argument: (indented_string_expression (string_fragment) @injection.content)) + (#match? @_filename "\\.(toml)$") + (#not-match? @_inner_func "(^|\\.)(removeSuffix|hasSuffix|hasPrefix|removePrefix|trace|throw|warn|warnIf|abort|assertMsg|seq|deepSeq|writeShellScript|writeShellScriptBin|runCommand|runCommandLocal|runCommandCC|runCommandNoCC)$") + (#set! injection.language "toml") + (#set! injection.combined)) + +((apply_expression + function: (apply_expression + function: (_) @_inner_func + argument: (string_expression (string_fragment) @_filename)) + argument: (indented_string_expression (string_fragment) @injection.content)) + (#match? @_filename "\\.(lua)$") + (#not-match? @_inner_func "(^|\\.)(removeSuffix|hasSuffix|hasPrefix|removePrefix|trace|throw|warn|warnIf|abort|assertMsg|seq|deepSeq|writeShellScript|writeShellScriptBin|runCommand|runCommandLocal|runCommandCC|runCommandNoCC)$") + (#set! injection.language "lua") + (#set! injection.combined)) + +((apply_expression + function: (apply_expression + function: (_) @_inner_func + argument: (string_expression (string_fragment) @_filename)) + argument: (indented_string_expression (string_fragment) @injection.content)) + (#match? @_filename "\\.(nix)$") + (#not-match? @_inner_func "(^|\\.)(removeSuffix|hasSuffix|hasPrefix|removePrefix|trace|throw|warn|warnIf|abort|assertMsg|seq|deepSeq|writeShellScript|writeShellScriptBin|runCommand|runCommandLocal|runCommandCC|runCommandNoCC)$") + (#set! injection.language "nix") + (#set! injection.combined)) + +((apply_expression + function: (apply_expression + function: (_) @_inner_func + argument: (string_expression (string_fragment) @_filename)) + argument: (indented_string_expression (string_fragment) @injection.content)) + (#match? @_filename "\\.(xml)$") + (#not-match? @_inner_func "(^|\\.)(removeSuffix|hasSuffix|hasPrefix|removePrefix|trace|throw|warn|warnIf|abort|assertMsg|seq|deepSeq|writeShellScript|writeShellScriptBin|runCommand|runCommandLocal|runCommandCC|runCommandNoCC)$") + (#set! injection.language "xml") + (#set! injection.combined)) + +((apply_expression + function: (apply_expression + function: (_) @_inner_func + argument: (string_expression (string_fragment) @_filename)) + argument: (indented_string_expression (string_fragment) @injection.content)) + (#match? @_filename "\\.(md)$") + (#not-match? @_inner_func "(^|\\.)(removeSuffix|hasSuffix|hasPrefix|removePrefix|trace|throw|warn|warnIf|abort|assertMsg|seq|deepSeq|writeShellScript|writeShellScriptBin|runCommand|runCommandLocal|runCommandCC|runCommandNoCC)$") + (#set! injection.language "markdown") + (#set! injection.combined)) + +((apply_expression + function: (apply_expression + function: (_) @_inner_func + argument: (string_expression (string_fragment) @_filename)) + argument: (indented_string_expression (string_fragment) @injection.content)) + (#match? @_filename "\\.(sql)$") + (#not-match? @_inner_func "(^|\\.)(removeSuffix|hasSuffix|hasPrefix|removePrefix|trace|throw|warn|warnIf|abort|assertMsg|seq|deepSeq|writeShellScript|writeShellScriptBin|runCommand|runCommandLocal|runCommandCC|runCommandNoCC)$") + (#set! injection.language "sql") + (#set! injection.combined)) ((apply_expression function: (apply_expression function: (_) @_func @@ -254,19 +452,6 @@ (#match? @_func "(^|\\.)write(Text|Script(Bin)?)$") (#set! injection.combined)) +; Let Helix infer a language from a shebang at the start of an indented string. ((indented_string_expression (string_fragment) @injection.shebang @injection.content) - (#set! injection.combined)) - -; string contents of lib.literalExpression is nix code -((apply_expression - function: [ - (select_expression) ; `lib.literalExpression` - (variable_expression) ; `literalExpression` this is the case when the symbol is brougth into scope e.g. `let inherit (lib) literalExpression; in` - ] @_func - argument: [ - (indented_string_expression (string_fragment) @injection.content) ; lib.literalExpression ''...'' - (string_expression (string_fragment) @injection.content) ; lib.literalExpression "..." - ]) - (#any-of? @_func "lib.literalExpression" "literalExpression") - (#set! injection.language "nix") - (#set! injection.combined)) + (#set! injection.combined)) diff --git a/runtime/queries/nix/locals.scm b/runtime/queries/nix/locals.scm new file mode 100644 index 00000000..607cf837 --- /dev/null +++ b/runtime/queries/nix/locals.scm @@ -0,0 +1,15 @@ +; When using @local.reference, tree-sitter seems to apply the scope from the +; identifier it has looked up, which makes sense for most languages. +; +; However, we want to highlight things as functions based on their call-site, +; not their definition. TS's support for tracking locals impedes our ability to +; get the highlighting we want. +; +; Also, TS doesn't seem to support scoping as implemented in languages with +; lazy let bindings, which results in syntax highlighting / goto-reference +; results that depend on the order of definitions. That is counter to the +; semantics of Nix. +; +; So for now we'll opt for not having any locals queries. +; +; See: https://github.com/tree-sitter/tree-sitter/issues/918 diff --git a/runtime/queries/nix/tags.scm b/runtime/queries/nix/tags.scm new file mode 100644 index 00000000..58188899 --- /dev/null +++ b/runtime/queries/nix/tags.scm @@ -0,0 +1,51 @@ +; https://tree-sitter.github.io/tree-sitter/4-code-navigation.html +; +; The Nix data model is "everything is an attrset of expressions"; +; functions and values share the same binding form. We capture +; definitions broadly and mark function-valued bindings as +; @definition.function specifically so code navigation can distinguish them. + +; ----- Definitions ----- + +; Generic binding - any top-level or nested attrset attribute. +; Anchor @name to the leaf of the attrpath so `foo.bar.baz = ...` +; tags `baz` rather than the whole dotted path. +(binding + attrpath: (attrpath + attr: (identifier) @name .) + expression: (_)) @definition.constant + +; Function-valued bindings - `foo = x: ...`. +(binding + attrpath: (attrpath + attr: (identifier) @name .) + expression: (function_expression)) @definition.function + +; inherit - definitions brought from another scope. +(inherit + attrs: (inherited_attrs attr: (identifier) @name)) @definition.constant + +(inherit_from + attrs: (inherited_attrs attr: (identifier) @name)) @definition.constant + +; ----- References ----- + +; Any bare identifier used as a value expression is a reference. +(variable_expression + name: (identifier) @name) @reference.constant + +; Function application - the thing being called is a reference.call. +(apply_expression + function: (variable_expression + name: (identifier) @name)) @reference.call + +; Method-style call: `foo.bar.baz arg` - tag the leaf attrname. +(apply_expression + function: (select_expression + attrpath: (attrpath + attr: (identifier) @name .))) @reference.call + +; Attribute access (non-call) - `foo.bar.baz` lookup. +(select_expression + attrpath: (attrpath + attr: (identifier) @name .)) @reference.constant