From c5cd3fc181ca3ae3cfc566de8305033736fb470b Mon Sep 17 00:00:00 2001 From: ninthcircle Date: Sat, 29 Aug 2026 02:33:44 +0530 Subject: [PATCH] feat: add support for SQL syntax highlighting --- assets/highlighting-tests/sql.sql | 305 ++++++++++++++++++++++++++++ crates/lsh/definitions/markdown.lsh | 10 + crates/lsh/definitions/sql.lsh | 79 +++++++ 3 files changed, 394 insertions(+) create mode 100644 assets/highlighting-tests/sql.sql create mode 100644 crates/lsh/definitions/sql.lsh diff --git a/assets/highlighting-tests/sql.sql b/assets/highlighting-tests/sql.sql new file mode 100644 index 00000000000..3c6e0ec387f --- /dev/null +++ b/assets/highlighting-tests/sql.sql @@ -0,0 +1,305 @@ +-- Single-line comment + +/* + * Multi-line comment. + * The comment should remain highlighted across lines. + */ + +-- Basic SELECT +SELECT + id, + name, + email +FROM + users +WHERE + active = TRUE; + +-- Aliases and expressions +SELECT + u.id AS user_id, + u.name AS user_name, + u.age + 1 AS next_age +FROM + users AS u +WHERE + u.age >= 18 + AND u.name IS NOT NULL; + +-- String literals +SELECT + 'hello', + 'It''s a string', + 'single quotes are escaped by doubling them'; + +-- Quoted identifiers +SELECT + "user", + "first""name" +FROM + "users"; + +-- Backtick-quoted identifiers +SELECT + `user`, + `first``name` +FROM + `users`; + +-- Numeric literals +SELECT + 0, + 42, + -17, + 3.14159, + 1.5e -3; + +-- Boolean and NULL literals +SELECT + TRUE, + FALSE, + NULL; + +-- Comparison and logical operators +SELECT + * +FROM + users +WHERE + age >= 18 + AND age <= 65 + AND status != 'inactive' + AND name <> 'Unknown' + AND ( + active = TRUE + OR verified = FALSE + ); + +-- LIKE, IN and BETWEEN +SELECT + * +FROM + users +WHERE + name LIKE 'A%' + AND age BETWEEN 18 + AND 30 + AND status IN ('active', 'pending'); + +-- INSERT +INSERT INTO + users (name, email, age) +VALUES + ('Alice', 'alice@example.com', 25); + +-- Multiple-row INSERT +INSERT INTO + users (name, email) +VALUES + ('Bob', 'bob@example.com'), + ('Carol', 'carol@example.com'); + +-- UPDATE +UPDATE + users +SET + name = 'Updated', + active = FALSE +WHERE + id = 42; + +-- DELETE +DELETE FROM + users +WHERE + active = FALSE; + +-- JOINs +SELECT + u.name, + o.id, + o.total +FROM + users AS u + INNER JOIN orders AS o ON o.user_id = u.id + LEFT JOIN payments AS p ON p.order_id = o.id +WHERE + o.total > 100; + +-- GROUP BY and aggregate functions +SELECT + department, + COUNT(*) AS employee_count, + SUM(salary) AS total_salary, + AVG(salary) AS average_salary, + MIN(salary) AS minimum_salary, + MAX(salary) AS maximum_salary +FROM + employees +GROUP BY + department +HAVING + COUNT(*) > 5 +ORDER BY + average_salary DESC; + +-- DISTINCT, LIMIT and OFFSET +SELECT + DISTINCT name +FROM + users +ORDER BY + name ASC +LIMIT + 10 OFFSET 20; + +-- CASE expression +SELECT + name, + CASE + WHEN age < 18 THEN 'minor' + WHEN age >= 18 + AND age < 65 THEN 'adult' + ELSE 'senior' + END AS age_group +FROM + users; + +-- Common functions +SELECT + LOWER(name), + UPPER(name), + LENGTH(name), + SUBSTRING(name, 1, 5), + ROUND(balance), + ABS(balance), + COALESCE(nickname, name), + NULLIF(status, ''); + +-- CAST and conversion +SELECT + CAST(age AS INTEGER), + CONVERT(name, VARCHAR(100)) +FROM + users; + +-- Common DDL +CREATE TABLE users ( + id INTEGER PRIMARY KEY, + name VARCHAR(100) NOT NULL, + email VARCHAR(255) UNIQUE, + age INTEGER DEFAULT 0, + active BOOLEAN DEFAULT TRUE +); + +-- Constraints +CREATE TABLE orders ( + id INTEGER PRIMARY KEY, + user_id INTEGER NOT NULL, + total DECIMAL(10, 2) CHECK (total >= 0), + CONSTRAINT fk_user FOREIGN KEY (user_id) REFERENCES users(id) +); + +-- Index +CREATE INDEX idx_users_email ON users (email); + +-- View +CREATE VIEW active_users AS +SELECT + id, + name, + email +FROM + users +WHERE + active = TRUE; + +-- ALTER and DROP +ALTER TABLE + users +ADD + COLUMN created_at TIMESTAMP; + +DROP VIEW active_users; + +-- Common transaction keywords +BEGIN; + +UPDATE + users +SET + active = TRUE +WHERE + id = 1; + +COMMIT; + +BEGIN; + +DELETE FROM + users +WHERE + id = 999; + +ROLLBACK; + +-- CTE +WITH active_users AS ( + SELECT + id, + name + FROM + users + WHERE + active = TRUE +) +SELECT + * +FROM + active_users +ORDER BY + name; + +-- UNION +SELECT + id, + name +FROM + users +UNION +SELECT + id, + name +FROM + archived_users; + +-- EXISTS +SELECT + * +FROM + users AS u +WHERE + EXISTS ( + SELECT + 1 + FROM + orders AS o + WHERE + o.user_id = u.id + ); + +-- NULL checks +SELECT + * +FROM + users +WHERE + email IS NULL + OR email IS NOT NULL; + +-- Function calls with nested expressions +SELECT + COALESCE(LOWER(name), 'unknown'), + ROUND(ABS(balance), 2) +FROM + accounts; diff --git a/crates/lsh/definitions/markdown.lsh b/crates/lsh/definitions/markdown.lsh index 2a8643406d0..2f789f826a1 100644 --- a/crates/lsh/definitions/markdown.lsh +++ b/crates/lsh/definitions/markdown.lsh @@ -50,6 +50,16 @@ pub fn markdown() { if /.*/ {} } } + } else if /(?i:sql)/ { + loop { + await input; + if /\s*```/ { + return; + } else { + sql(); + if /.*/ {} + } + } } else if /(?i:json)/ { loop { await input; diff --git a/crates/lsh/definitions/sql.lsh b/crates/lsh/definitions/sql.lsh new file mode 100644 index 00000000000..8b1a2b151ac --- /dev/null +++ b/crates/lsh/definitions/sql.lsh @@ -0,0 +1,79 @@ +#[display_name = "SQL"] +#[path = "**/*.sql"] +#[path = "**/*.mssql"] +#[path = "**/*.pgsql"] +pub fn sql() { + until /$/ { + yield other; + + // Single-line comments. + if /--.*/ { + yield comment; + // Multi-line comments. + } else if /\/\*/ { + loop { + yield comment; + await input; + if /\*\// { + yield comment; + break; + } + } + } else if /'/ { + // Single-quoted strings. + loop { + yield string; + if /''/ { + // SQL escapes a single quote by doubling it. + } else if /'/ { + yield string; + break; + } + await input; + } + } else if /"/ { + // Double-quoted identifiers. + loop { + yield string; + if /""/ { + // SQL escapes a double quote by doubling it. + } else if /"/ { + yield string; + break; + } + await input; + } + } else if /`/ { + // Backtick-quoted identifiers. + loop { + yield string; + if /``/ { + // SQL escapes a backtick by doubling it. + } else if /`/ { + yield string; + break; + } + await input; + } + } else if /(?i:(?:SELECT|FROM|WHERE|INSERT|INTO|VALUES|UPDATE|SET|DELETE|CREATE|ALTER|DROP|TRUNCATE|MERGE|CALL|RETURNING|WITH|RECURSIVE|UNION|INTERSECT|EXCEPT|CASE|WHEN|THEN|ELSE|END|BEGIN|COMMIT|ROLLBACK))\>/ { + // Query and control-flow keywords. + yield keyword.control; + } else if /(?i:(?:AS|AND|OR|NOT|IS|IN|BETWEEN|LIKE|ILIKE|EXISTS|ALL|ANY|DISTINCT|GROUP|BY|HAVING|ORDER|ASC|DESC|LIMIT|OFFSET|FETCH|JOIN|INNER|LEFT|RIGHT|FULL|CROSS|OUTER|ON|USING|PRIMARY|KEY|FOREIGN|REFERENCES|UNIQUE|CHECK|DEFAULT|CONSTRAIT|TABLE|VIEW|INDEX|DATABASE|SCHEMA|GRANT|REVOKE|CASCADE|RESTRICT|ADD|COLUMN))\>/ { + // Other SQL keywords. + yield keyword.other; + } else if /(?i:(?:TRUE|FALSE|NULL))\>/ { + // Boolean and NULL literals. + yield constant.language; + } else if /-?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?\>/ { + // Numeric literals. + yield constant.numeric; + } else if /(?i:(COUNT|SUM|AVG|MIN|MAX|COALESCE|NULLIF|CAST|CONVERT|LOWER|UPPER|LENGTH|SUBSTRING|ROUND|ABS|NOW|CURRENT_DATE|CURRENT_TIME|CURRENT_TIMESTAMP))\s*\(/ { + // Common SQL functions. + yield $1 as method; + } else if /\w+/ { + // Gobble word chars to align the next iteration on a word boundary. + } + + yield other; + } +}