Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "surrealql",
"displayName": "SurrealQL",
"description": "Language support for SurrealQL — syntax highlighting, language server, run-query code lens, and snippets",
"version": "0.4.1",
"version": "0.4.2",
"publisher": "surrealdb",
"icon": "./icon.png",
"author": {
Expand All @@ -15,14 +15,15 @@
"scripts": {
"typecheck": "tsc --noEmit -p tsconfig.json",
"build": "node esbuild.config.mjs",
"build:grammar": "node scripts/build-keywords.mjs",
"build:watch": "node esbuild.config.mjs --watch",
"vscode:prepublish": "node esbuild.config.mjs --production",
"lint:check": "biome check . --formatter-enabled=false",
"lint:apply": "biome check . --formatter-enabled=false --write",
"lint:apply:unsafe": "biome check . --formatter-enabled=false --write --unsafe",
"test": "bun test test/textmate",
"test:textmate": "bun test test/textmate",
"validate": "bun run typecheck && biome check . && bun run test && bun run build"
"validate": "bun run build:grammar && bun run typecheck && biome check . && bun run test && bun run build"
},
"repository": {
"type": "git",
Expand Down
1 change: 1 addition & 0 deletions raw/keywords.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ API
AS
ASC
ASSERT
ASYNC
AT
AUTHENTICATE
AUTO
Expand Down
45 changes: 45 additions & 0 deletions scripts/build-keywords.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
#!/usr/bin/env node
/**
* Regenerate TextMate keyword patterns from raw/keywords.txt.
* Splits into smaller alternations (longest-first) for reliable Oniguruma matching.
*/

import { readFileSync, writeFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";

const root = join(dirname(fileURLToPath(import.meta.url)), "..");
const keywordsPath = join(root, "raw/keywords.txt");
const grammarPath = join(root, "syntaxes/surrealql.tmLanguage.json");

/** Keywords handled elsewhere or too ambiguous as case-insensitive reserved words. */
const EXCLUDE = new Set(["and", "or", "post"]);

const CHUNK_SIZE = 35;

const keywords = [
...new Set([
...readFileSync(keywordsPath, "utf8")
.split("\n")
.map((line) => line.trim().toLowerCase())
.filter((word) => word && !EXCLUDE.has(word)),
]),
].sort((a, b) => b.length - a.length);

const patterns = [];
for (let i = 0; i < keywords.length; i += CHUNK_SIZE) {
const chunk = keywords.slice(i, i + CHUNK_SIZE);
patterns.push({
name: "keyword.control keyword.control.surrealql",
match: `(?i)\\b(${chunk.join("|")})\\b`,
});
}

const grammar = JSON.parse(readFileSync(grammarPath, "utf8"));
grammar.repository.keywords.patterns = patterns;

writeFileSync(grammarPath, `${JSON.stringify(grammar, null, "\t")}\n`);

console.log(
`Updated ${grammarPath}: ${patterns.length} keyword patterns, ${keywords.length} keywords`,
);
4 changes: 4 additions & 0 deletions snippets.json
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,10 @@
"prefix": "define event",
"body": ["DEFINE EVENT $1 ON $2 WHEN ($3) THEN {", "\t$4", "};"]
},
"Define an async event": {
"prefix": "define event async",
"body": ["DEFINE EVENT $1 ON $2 WHEN ($3) ASYNC THEN {", "\t$4", "};"]
},
"Define a function": {
"prefix": "define function",
"body": ["DEFINE FUNCTION fn::$1($2) {", "\t$3", "};"]
Expand Down
24 changes: 22 additions & 2 deletions syntaxes/surrealql.tmLanguage.json
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,28 @@
"keywords": {
"patterns": [
{
"name": "keyword.control.surrealql",
"match": "(?i)\\b(keep_pruned_connections|doc_lengths_cache|doc_lengths_order|extend_candidates|postings_cache|postings_order|doc_ids_cache|doc_ids_order|authenticate|concurrently|mtree_cache|permissions|terms_cache|terms_order|transaction|changefeed|highlights|middleware|schemafull|schemaless|tokenizers|algorithm|dimension|duplicate|functions|namespace|overwrite|reference|structure|tempfiles|analyzer|capacity|computed|continue|database|duration|enforced|flexible|parallel|passhash|password|readonly|relation|function|backend|cascade|changes|collate|columns|comment|content|default|exclude|explain|expunge|filters|graphql|include|noindex|numeric|rebuild|replace|session|timeout|version|access|always|assert|bucket|cancel|commit|config|create|define|delete|exists|fields|ignore|insert|issuer|normal|option|record|reject|relate|remove|return|search|select|signin|signup|strict|tables|unique|update|upsert|values|alter|begin|break|defer|event|fetch|field|group|index|limit|merge|mtree|order|param|patch|roles|scope|since|sleep|split|start|table|throw|token|trace|unset|value|where|auto|bm25|desc|dist|drop|else|from|hnsw|info|into|kill|live|omit|only|root|show|then|type|user|when|with|all|any|api|asc|efc|end|for|get|jwt|key|let|not|out|put|set|url|use|as|at|by|db|if|in|lm|m0|ns|on|sc|tb|to|m)\\b"
"name": "keyword.control keyword.control.surrealql",
"match": "(?i)\\b(keep_pruned_connections|doc_lengths_cache|doc_lengths_order|extend_candidates|postings_cache|postings_order|doc_ids_cache|doc_ids_order|authenticate|concurrently|mtree_cache|permissions|terms_cache|terms_order|transaction|changefeed|highlights|middleware|schemafull|schemaless|tokenizers|algorithm|dimension|duplicate|functions|namespace|overwrite|reference|structure|tempfiles|analyzer|capacity|computed|continue|database)\\b"
},
{
"name": "keyword.control keyword.control.surrealql",
"match": "(?i)\\b(duration|enforced|flexible|function|parallel|passhash|password|readonly|relation|backend|cascade|changes|collate|columns|comment|content|default|exclude|explain|expunge|filters|graphql|include|noindex|numeric|rebuild|replace|session|timeout|version|access|always|assert|bucket|cancel)\\b"
},
{
"name": "keyword.control keyword.control.surrealql",
"match": "(?i)\\b(commit|config|create|define|delete|exists|fields|ignore|insert|issuer|normal|option|record|reject|relate|remove|return|search|select|signin|signup|strict|tables|unique|update|upsert|values|alter|async|begin|break|defer|event|fetch|field)\\b"
},
{
"name": "keyword.control keyword.control.surrealql",
"match": "(?i)\\b(group|index|limit|merge|mtree|order|param|patch|roles|scope|since|sleep|split|start|table|throw|token|trace|unset|value|where|auto|bm25|desc|dist|drop|else|from|hnsw|info|into|kill|live|omit|only)\\b"
},
{
"name": "keyword.control keyword.control.surrealql",
"match": "(?i)\\b(root|show|then|type|user|when|with|all|any|api|asc|efc|end|for|get|jwt|key|let|not|out|put|set|url|use|as|at|by|db|if|in|lm|m0|ns|on|sc)\\b"
},
{
"name": "keyword.control keyword.control.surrealql",
"match": "(?i)\\b(tb|to|m)\\b"
}
]
},
Expand Down
17 changes: 17 additions & 0 deletions test/textmate/textmate.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { describe, expect, test } from "bun:test";
import { INITIAL } from "vscode-textmate";
import {
grammar,
lineTokenParts,
scopeAtColumn,
scopeForSubstring,
Expand All @@ -15,6 +17,21 @@ describe("SurrealQL TextMate grammar", () => {
expect(scopeAtColumn("DEFINE TABLE x", 7)).toBe("keyword.control.surrealql");
});

test("DEFINE EVENT ASYNC keyword", () => {
const line = "DEFINE EVENT e ON TABLE file WHEN $event = 'DELETE' ASYNC THEN { };";
expect(scopeForSubstring(line, "ASYNC")).toBe("keyword.control.surrealql");
expect(scopeForSubstring(line, "WHEN")).toBe("keyword.control.surrealql");
expect(scopeForSubstring(line, "THEN")).toBe("keyword.control.surrealql");
});

test("ASYNC uses standard keyword.control scope for theme compatibility", () => {
const line = "DEFINE EVENT e ON TABLE t WHEN $event = 'DELETE' ASYNC THEN { };";
const { tokens } = grammar.tokenizeLine(line, INITIAL);
const asyncToken = tokens.find((t) => line.slice(t.startIndex, t.endIndex) === "ASYNC");
expect(asyncToken?.scopes).toContain("keyword.control");
expect(asyncToken?.scopes).toContain("keyword.control.surrealql");
});

test("identifiers (not reserved words)", () => {
expect(scopeAtColumn("FROM person", 5)).toBe("variable.other.surrealql");
expect(scopeForSubstring("DEFINE TABLE post SCHEMALESS", "post")).toBe(
Expand Down
Loading