From e34111256bbd6033fcb40bd8ee056bb6df78142f Mon Sep 17 00:00:00 2001 From: Ben Woodward <1472981+benwoodward@users.noreply.github.com> Date: Fri, 12 Dec 2025 16:00:32 +1100 Subject: [PATCH 1/6] feat: add HelixQL parser Add parser, queries, and tests for HelixQL (helix-db.com), a SQL-like query language for HelixDB. --- SUPPORTED_LANGUAGES.md | 1 + lua/nvim-treesitter/parsers.lua | 8 + runtime/queries/helixql/folds.scm | 12 ++ runtime/queries/helixql/highlights.scm | 259 +++++++++++++++++++++++++ tests/query/highlights/helixql/test.hx | 122 ++++++++++++ 5 files changed, 402 insertions(+) create mode 100644 runtime/queries/helixql/folds.scm create mode 100644 runtime/queries/helixql/highlights.scm create mode 100644 tests/query/highlights/helixql/test.hx diff --git a/SUPPORTED_LANGUAGES.md b/SUPPORTED_LANGUAGES.md index b2c006f84..5be13e46e 100644 --- a/SUPPORTED_LANGUAGES.md +++ b/SUPPORTED_LANGUAGES.md @@ -123,6 +123,7 @@ ecma (queries only)[^ecma] | unstable | `HFIJL` | @steelsojka [haskell_persistent](https://github.com/MercuryTechnologies/tree-sitter-haskell-persistent) | unstable | `HF   ` | @lykahb [hcl](https://github.com/tree-sitter-grammars/tree-sitter-hcl) | unstable | `HFIJ ` | @MichaHoffmann [heex](https://github.com/connorlay/tree-sitter-heex) | unstable | `HFIJL` | @connorlay +[helixql](https://github.com/benwoodward/tree-sitter-helixql) | unsupported | `HF   ` | @benwoodward [helm](https://github.com/ngalaiko/tree-sitter-go-template) | unstable | `HF JL` | @qvalentin [hjson](https://github.com/winston0410/tree-sitter-hjson) | unstable | `HFIJL` | @winston0410 [hlsl](https://github.com/tree-sitter-grammars/tree-sitter-hlsl) | unstable | `HFIJL` | @theHamsta diff --git a/lua/nvim-treesitter/parsers.lua b/lua/nvim-treesitter/parsers.lua index 0f9fc5057..8fc80996b 100644 --- a/lua/nvim-treesitter/parsers.lua +++ b/lua/nvim-treesitter/parsers.lua @@ -912,6 +912,14 @@ return { maintainers = { '@qvalentin' }, tier = 2, }, + helixql = { + install_info = { + branch = 'main', + url = 'https://github.com/benwoodward/tree-sitter-helixql', + }, + maintainers = { '@benwoodward' }, + tier = 4, + }, hjson = { install_info = { revision = '02fa3b79b3ff9a296066da6277adfc3f26cbc9e0', diff --git a/runtime/queries/helixql/folds.scm b/runtime/queries/helixql/folds.scm new file mode 100644 index 000000000..6648551f8 --- /dev/null +++ b/runtime/queries/helixql/folds.scm @@ -0,0 +1,12 @@ +; ============================================================================= +; HelixQL Folds for Neovim Tree-sitter +; ============================================================================= +[ + (node_def) + (edge_def) + (vector_def) + (query_def) + (for_loop) + (object_step) + (create_field) +] @fold diff --git a/runtime/queries/helixql/highlights.scm b/runtime/queries/helixql/highlights.scm new file mode 100644 index 000000000..20fc91ab6 --- /dev/null +++ b/runtime/queries/helixql/highlights.scm @@ -0,0 +1,259 @@ +; ============================================================================= +; HelixQL Syntax Highlighting for Neovim Tree-sitter +; ============================================================================= +; NOTE: In tree-sitter queries, later patterns override earlier ones. +; Put general patterns first, then specific overrides after. +; ----------------------------------------------------------------------------- +; Comments +; ----------------------------------------------------------------------------- +(comment) @comment @spell + +; ----------------------------------------------------------------------------- +; Literals +; ----------------------------------------------------------------------------- +(string_literal) @string + +(integer) @number + +(float) @number.float + +(boolean) @boolean + +(none) @constant.builtin + +(now) @constant.builtin + +; ----------------------------------------------------------------------------- +; Generic identifiers +; ----------------------------------------------------------------------------- +(identifier) @variable + +(identifier_upper) @type + +; ----------------------------------------------------------------------------- +; Keywords +; ----------------------------------------------------------------------------- +[ + "QUERY" + "RETURN" + "FOR" + "IN" +] @keyword + +; Node type keywords (wrapped in their own rules) +(index) @keyword + +(default) @keyword + +(count) @keyword + +(ID) @constant.builtin + +; Keywords inside node types +(where_step + "WHERE" @keyword) + +(range_step + "RANGE" @keyword) + +(update + "UPDATE" @keyword) + +(drop + "DROP" @keyword) + +(exists + "EXISTS" @keyword) + +; Boolean and logical operators +(and + "AND" @keyword.operator) + +(or + "OR" @keyword.operator) + +(GT + "GT" @keyword.operator) + +(GTE + "GTE" @keyword.operator) + +(LT + "LT" @keyword.operator) + +(LTE + "LTE" @keyword.operator) + +(EQ + "EQ" @keyword.operator) + +(NEQ + "NEQ" @keyword.operator) + +; Creation operation keywords +(AddN + "AddN" @function.builtin) + +(AddE + "AddE" @function.builtin) + +(AddV + "AddV" @function.builtin) + +(BatchAddV + "BatchAddV" @function.builtin) + +(search_vector + "SearchV" @function.builtin) + +; ----------------------------------------------------------------------------- +; Schema definition keywords +; ----------------------------------------------------------------------------- +[ + "N::" + "E::" + "V::" +] @keyword.directive + +[ + "From:" + "To:" + "Properties" +] @keyword.modifier + +; ----------------------------------------------------------------------------- +; Graph traversal methods +; ----------------------------------------------------------------------------- +(out_e + "OutE" @function.method) + +(in_e + "InE" @function.method) + +(from_n) @function.method + +(to_n) @function.method + +(out + "Out" @function.method) + +(in_nodes + "In" @function.method) + +(shortest_path + "ShortestPath" @function.method) + +; ----------------------------------------------------------------------------- +; Start nodes - highlight N, E, V as keywords +; ----------------------------------------------------------------------------- +(start_node + "N" @keyword) + +(start_edge + "E" @keyword) + +(start_vector + "V" @keyword) + +; ----------------------------------------------------------------------------- +; Types +; ----------------------------------------------------------------------------- +(named_type) @type.builtin + +(date_type) @type.builtin + +(ID_TYPE) @type.builtin + +; Type arguments in angle brackets +(type_args + (identifier_upper) @type) + +; ----------------------------------------------------------------------------- +; Specific identifier patterns (override generic rules) +; ----------------------------------------------------------------------------- +; Query/function names +(query_def + name: (identifier) @function) + +; Schema definition names +(node_def + name: (identifier_upper) @type.definition) + +(edge_def + name: (identifier_upper) @type.definition) + +(vector_def + name: (identifier_upper) @type.definition) + +; Parameter names in function signatures +(param_def + name: (identifier) @variable.parameter) + +; Field names in schema definitions +(field_def + name: (identifier) @property) + +; Property keys in object creation +(new_field + key: (identifier) @property) + +; Property keys in updates +(update_field + key: (identifier) @property) + +; Property keys in object mapping (left of :) +(mapping_field + (identifier) @property + ":") + +; Object access field +(object_access + field: (identifier) @property) + +; Variable assignment targets (left of <-) +(get_stmt + variable: (identifier) @variable) + +; For loop iterable +(for_loop + iterable: (identifier) @variable) + +; ----------------------------------------------------------------------------- +; Operators +; ----------------------------------------------------------------------------- +[ + "<-" + "=>" + "::" + "." + ".." + "!" +] @operator + +; ----------------------------------------------------------------------------- +; Punctuation +; ----------------------------------------------------------------------------- +[ + ":" + "," +] @punctuation.delimiter + +[ + "(" + ")" + "{" + "}" + "[" + "]" + "<" + ">" +] @punctuation.bracket + +"|" @punctuation.special + +; ----------------------------------------------------------------------------- +; Special patterns +; ----------------------------------------------------------------------------- +; Anonymous traversal underscore +(anonymous_traversal + "_" @variable.builtin) diff --git a/tests/query/highlights/helixql/test.hx b/tests/query/highlights/helixql/test.hx new file mode 100644 index 000000000..d65ba6ceb --- /dev/null +++ b/tests/query/highlights/helixql/test.hx @@ -0,0 +1,122 @@ +// Schema definitions +// <- @comment +N::User { +// <- @keyword.directive +// ^ @type.definition + INDEX id: ID, +//^ @keyword +// ^ @type.builtin + name: String, +//^ @property +// ^ @type.builtin + email: String, + age: I32, +// ^ @type.builtin + created_at: Date DEFAULT NOW +// ^ @type.builtin +// ^ @keyword +// ^ @constant.builtin +} + +E::Follows { +// <- @keyword.directive + From: User, +//^ @keyword.modifier + To: User, +//^ @keyword.modifier + Properties: { +//^ @keyword.modifier + since: Date + } +} + +V::UserEmbedding { +// <- @keyword.directive + user_id: ID, + embedding: [F32] +} + +// Simple query +QUERY getUser(userId: ID) => +// <- @keyword +// ^ @function +// ^ @variable.parameter + user <- N(userId) +//^ @variable +// ^ @operator +// ^ @keyword +// ^ @type + RETURN user +//^ @keyword + +// Query with traversal +QUERY getFollowers(userId: ID) => + user <- N(userId) + followers <- user::InE::FromN +// ^ @function.method +// ^ @function.method + RETURN followers + +// Query with filtering +QUERY getAdults(minAge: I32) => + users <- N::WHERE(_::age::GTE(minAge)) +// ^ @keyword +// ^ @variable.builtin +// ^ @keyword.operator + RETURN users + +// Query with object return +QUERY getUserStats(userId: ID) => + user <- N(userId) + followCount <- user::OutE::COUNT +// ^ @keyword + RETURN { + userId: userId, +// ^ @property + followCount: followCount + } + +// Query with AddN +QUERY createUser(name: String, email: String) => + user <- AddN({ +// ^ @function.builtin + name: name, + email: email + }) + RETURN user + +// Query with AddE +QUERY followUser(fromId: ID, toId: ID) => + from <- N(fromId) + to <- N(toId) + AddE::From(from::ID)::To(to::ID) +//^ @function.builtin + RETURN from + +// Query with FOR loop +QUERY createMultipleUsers(names: [String]) => + FOR name IN names { +//^ @keyword +// ^ @keyword + user <- AddN({name: name}) + } + RETURN NONE +// ^ @constant.builtin + +// Vector search +QUERY findSimilar(embedding: [F32], limit: I32) => + results <- SearchV(embedding, limit) +// ^ @function.builtin + RETURN results + +// Literals +QUERY testLiterals() => + str <- "hello world" +// ^ @string + num <- 42 +// ^ @number + flt <- 3.14 +// ^ @number.float + b <- true +// ^ @boolean + RETURN NONE From 1a5c29bc8bca533f6880241c2a3e64d87670f564 Mon Sep 17 00:00:00 2001 From: Ben Woodward <1472981+benwoodward@users.noreply.github.com> Date: Fri, 12 Dec 2025 16:22:09 +1100 Subject: [PATCH 2/6] fix: add revision field and filetype mapping for helixql --- lua/nvim-treesitter/parsers.lua | 2 +- plugin/filetypes.lua | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/lua/nvim-treesitter/parsers.lua b/lua/nvim-treesitter/parsers.lua index 8fc80996b..a3c52eaf6 100644 --- a/lua/nvim-treesitter/parsers.lua +++ b/lua/nvim-treesitter/parsers.lua @@ -914,7 +914,7 @@ return { }, helixql = { install_info = { - branch = 'main', + revision = 'f6c799534d46947204f6db962d0aae7b9e46d2cb', url = 'https://github.com/benwoodward/tree-sitter-helixql', }, maintainers = { '@benwoodward' }, diff --git a/plugin/filetypes.lua b/plugin/filetypes.lua index 7759c1f5c..3d49ba514 100644 --- a/plugin/filetypes.lua +++ b/plugin/filetypes.lua @@ -20,6 +20,7 @@ local filetypes = { godot_resource = { 'gdresource' }, haskell = { 'hs' }, haskell_persistent = { 'haskellpersistent' }, + helixql = { 'hx' }, idris = { 'idris2' }, ini = { 'confini', 'dosini' }, janet_simple = { 'janet' }, From e33465283f82268ab3184e5a049978dfa08b5b3f Mon Sep 17 00:00:00 2001 From: Ben Woodward <1472981+benwoodward@users.noreply.github.com> Date: Fri, 12 Dec 2025 17:12:54 +1100 Subject: [PATCH 3/6] fix: change helixql to tier 2 (unstable) Tier 4 parsers are excluded from CI compilation, which is why the parser wasn't being built and queries couldn't be validated. --- SUPPORTED_LANGUAGES.md | 2 +- lua/nvim-treesitter/parsers.lua | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/SUPPORTED_LANGUAGES.md b/SUPPORTED_LANGUAGES.md index 5be13e46e..d0587d889 100644 --- a/SUPPORTED_LANGUAGES.md +++ b/SUPPORTED_LANGUAGES.md @@ -123,7 +123,7 @@ ecma (queries only)[^ecma] | unstable | `HFIJL` | @steelsojka [haskell_persistent](https://github.com/MercuryTechnologies/tree-sitter-haskell-persistent) | unstable | `HF   ` | @lykahb [hcl](https://github.com/tree-sitter-grammars/tree-sitter-hcl) | unstable | `HFIJ ` | @MichaHoffmann [heex](https://github.com/connorlay/tree-sitter-heex) | unstable | `HFIJL` | @connorlay -[helixql](https://github.com/benwoodward/tree-sitter-helixql) | unsupported | `HF   ` | @benwoodward +[helixql](https://github.com/benwoodward/tree-sitter-helixql) | unstable | `HF   ` | @benwoodward [helm](https://github.com/ngalaiko/tree-sitter-go-template) | unstable | `HF JL` | @qvalentin [hjson](https://github.com/winston0410/tree-sitter-hjson) | unstable | `HFIJL` | @winston0410 [hlsl](https://github.com/tree-sitter-grammars/tree-sitter-hlsl) | unstable | `HFIJL` | @theHamsta diff --git a/lua/nvim-treesitter/parsers.lua b/lua/nvim-treesitter/parsers.lua index a3c52eaf6..aaa46926c 100644 --- a/lua/nvim-treesitter/parsers.lua +++ b/lua/nvim-treesitter/parsers.lua @@ -918,7 +918,7 @@ return { url = 'https://github.com/benwoodward/tree-sitter-helixql', }, maintainers = { '@benwoodward' }, - tier = 4, + tier = 2, }, hjson = { install_info = { From 1cdd2c32f9bbfff0cec21de0b139e691b5e08d4f Mon Sep 17 00:00:00 2001 From: Ben Woodward <1472981+benwoodward@users.noreply.github.com> Date: Fri, 12 Dec 2025 17:55:41 +1100 Subject: [PATCH 4/6] fix: resolve .hx extension conflict with Haxe Neovim's built-in filetype detection maps .hx to 'haxe', which caused the test framework to attempt loading haxe.so instead of helixql.so. Changes: - Renamed test file from test.hx to test.helixql - Added vim modeline to ensure correct filetype detection - Removed helixql filetype mapping (not needed and doesn't override Neovim's built-in detection anyway) --- plugin/filetypes.lua | 1 - tests/query/highlights/helixql/{test.hx => test.helixql} | 2 ++ 2 files changed, 2 insertions(+), 1 deletion(-) rename tests/query/highlights/helixql/{test.hx => test.helixql} (98%) diff --git a/plugin/filetypes.lua b/plugin/filetypes.lua index 3d49ba514..7759c1f5c 100644 --- a/plugin/filetypes.lua +++ b/plugin/filetypes.lua @@ -20,7 +20,6 @@ local filetypes = { godot_resource = { 'gdresource' }, haskell = { 'hs' }, haskell_persistent = { 'haskellpersistent' }, - helixql = { 'hx' }, idris = { 'idris2' }, ini = { 'confini', 'dosini' }, janet_simple = { 'janet' }, diff --git a/tests/query/highlights/helixql/test.hx b/tests/query/highlights/helixql/test.helixql similarity index 98% rename from tests/query/highlights/helixql/test.hx rename to tests/query/highlights/helixql/test.helixql index d65ba6ceb..a13d5410b 100644 --- a/tests/query/highlights/helixql/test.hx +++ b/tests/query/highlights/helixql/test.helixql @@ -120,3 +120,5 @@ QUERY testLiterals() => b <- true // ^ @boolean RETURN NONE + +// vim: set filetype=helixql: From 5d2914b94071196c2552d555857c71ed98d7ac9b Mon Sep 17 00:00:00 2001 From: Ben Woodward <1472981+benwoodward@users.noreply.github.com> Date: Fri, 12 Dec 2025 17:58:57 +1100 Subject: [PATCH 5/6] chore: update helixql parser revision Updated to 259d2b68 which fixes the symlink issue that was breaking Windows CI during tarball extraction. --- lua/nvim-treesitter/parsers.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/nvim-treesitter/parsers.lua b/lua/nvim-treesitter/parsers.lua index aaa46926c..5675fd9e4 100644 --- a/lua/nvim-treesitter/parsers.lua +++ b/lua/nvim-treesitter/parsers.lua @@ -914,7 +914,7 @@ return { }, helixql = { install_info = { - revision = 'f6c799534d46947204f6db962d0aae7b9e46d2cb', + revision = '259d2b68c56ae40ef7d190919ed49e0f17d1f3ee', url = 'https://github.com/benwoodward/tree-sitter-helixql', }, maintainers = { '@benwoodward' }, From c2632c3b2d10aef82415136e4bcfa899ea613899 Mon Sep 17 00:00:00 2001 From: Ben Woodward <1472981+benwoodward@users.noreply.github.com> Date: Fri, 12 Dec 2025 18:14:10 +1100 Subject: [PATCH 6/6] fix: correct test assertions for helixql highlights Fixed column positions in test assertions to match actual parser output: - Line 6: moved ^ to point at ID type instead of colon delimiter - Line 98: moved ^ to point at IN keyword instead of name variable --- tests/query/highlights/helixql/test.helixql | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/query/highlights/helixql/test.helixql b/tests/query/highlights/helixql/test.helixql index a13d5410b..d61c1a653 100644 --- a/tests/query/highlights/helixql/test.helixql +++ b/tests/query/highlights/helixql/test.helixql @@ -5,7 +5,7 @@ N::User { // ^ @type.definition INDEX id: ID, //^ @keyword -// ^ @type.builtin +// ^ @type.builtin name: String, //^ @property // ^ @type.builtin @@ -97,7 +97,7 @@ QUERY followUser(fromId: ID, toId: ID) => QUERY createMultipleUsers(names: [String]) => FOR name IN names { //^ @keyword -// ^ @keyword +// ^ @keyword user <- AddN({name: name}) } RETURN NONE