From 4d0d2c4fab4acd19d4121302c2d50f8c6f5b5f9c Mon Sep 17 00:00:00 2001 From: qutianyu Date: Thu, 14 May 2026 23:42:25 +0800 Subject: [PATCH] feat(editor): support jsonc --- README.md | 19 +++- src/modules/editor/lib/jsonc.ts | 105 +++++++++++++++++++++ src/modules/editor/lib/languageResolver.ts | 1 + 3 files changed, 120 insertions(+), 5 deletions(-) create mode 100644 src/modules/editor/lib/jsonc.ts diff --git a/README.md b/README.md index d2774040..fcccc63a 100644 --- a/README.md +++ b/README.md @@ -2,14 +2,14 @@ Terax

Terax

-

Open-source lightweight cross-platform AI-native terminal (ADE)

+

Open-source lightweight cross-platform AI-native terminal (ADE)

-

+

version license platform -

+

--- @@ -31,25 +31,30 @@ Terax is a fast, lightweight AI terminal (ADE) built on Tauri 2 + Rust and React ## Features **Terminal** + - xterm.js + WebGL renderer, multi-tab with background streaming - Native PTY backend via `portable-pty` (zsh, bash, pwsh, …) - Shell integration (cwd reporting, prompt markers) via injected init scripts - Inline search, link detection, true-color **Editor** -- CodeMirror 6 with language support for TS/JS, Rust, Python, HTML/CSS, JSON, Markdown + +- CodeMirror 6 with language support for TS/JS, Rust, Python, HTML/CSS, JSON(JSONC), Markdown - Inline AI autocomplete and AI edit diffs - Vim mode - Prebuilt themes: Tokyo Night, Nord, GitHub, Atom One, Aura, Copilot, Xcode **File Explorer** + - Catppuccin icon theme (Material Icon Theme resolver) - Fuzzy search, keyboard navigation, inline rename, context actions **Web Preview** + - Auto-detects local dev servers and opens them in a preview tab **AI (BYOK)** + - Providers: OpenAI, Anthropic, Google, Groq, xAI, Cerebras, OpenAI-compatible - Local / offline models via LM Studio - Voice input, edit diffs, multi-agent and sub-agents @@ -58,8 +63,9 @@ Terax is a fast, lightweight AI terminal (ADE) built on Tauri 2 + Rust and React - Tasks, plans, search, file read/write tools with approval flow **Quality** + - Lightweight and fast (~7 MB bundle) -- API keys stored in the OS keychain +- API keys stored in the OS keychain - No telemetry, no account required ## Windows notes @@ -82,11 +88,13 @@ The default shell is detected in this order: `pwsh.exe` (PowerShell 7+) → `pow ## Build from source **Prerequisites** + - Rust (stable) — https://rustup.rs - Node 20+ and [pnpm](https://pnpm.io) - Platform-specific Tauri prerequisites — https://tauri.app/start/prerequisites/ **Run** + ```bash pnpm install pnpm tauri dev # development @@ -94,6 +102,7 @@ pnpm tauri build # production bundle ``` **Checks** + ```bash pnpm exec tsc --noEmit # frontend type-check cd src-tauri && cargo clippy # Rust lint diff --git a/src/modules/editor/lib/jsonc.ts b/src/modules/editor/lib/jsonc.ts new file mode 100644 index 00000000..6705feca --- /dev/null +++ b/src/modules/editor/lib/jsonc.ts @@ -0,0 +1,105 @@ +import type { StreamParser, StringStream } from "@codemirror/language"; + +type JsoncState = { + inBlockComment: boolean; +}; + +function readString(stream: StringStream) { + let escaped = false; + stream.next(); + + while (!stream.eol()) { + const ch = stream.next(); + if (escaped) { + escaped = false; + } else if (ch === "\\") { + escaped = true; + } else if (ch === '"') { + break; + } + } +} + +function skipInlineWhitespaceAndComments(line: string, pos: number): number { + for (;;) { + while (pos < line.length && /\s/.test(line[pos]!)) pos += 1; + + if (line.startsWith("//", pos)) return line.length; + + if (line.startsWith("/*", pos)) { + const end = line.indexOf("*/", pos + 2); + if (end === -1) return line.length; + pos = end + 2; + continue; + } + + return pos; + } +} + +function isPropertyName(stream: StringStream): boolean { + const pos = skipInlineWhitespaceAndComments(stream.string, stream.pos); + return stream.string[pos] === ":"; +} + +export const jsonc: StreamParser = { + name: "jsonc", + + startState() { + return { inBlockComment: false }; + }, + + token(stream, state) { + if (state.inBlockComment) { + if (stream.skipTo("*/")) { + stream.pos += 2; + state.inBlockComment = false; + } else { + stream.skipToEnd(); + } + return "comment"; + } + + if (stream.eatSpace()) return null; + + if (stream.match("//")) { + stream.skipToEnd(); + return "comment"; + } + + if (stream.match("/*")) { + if (stream.skipTo("*/")) { + stream.pos += 2; + } else { + state.inBlockComment = true; + stream.skipToEnd(); + } + return "comment"; + } + + const ch = stream.peek(); + + if (ch === '"') { + readString(stream); + return isPropertyName(stream) ? "propertyName" : "string"; + } + + if (stream.match(/^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?/)) { + return "number"; + } + + if (stream.match("true") || stream.match("false")) return "bool"; + if (stream.match("null")) return "keyword"; + + if (stream.eat(/[{}\[\]:,]/)) return "punctuation"; + + stream.next(); + return null; + }, + + languageData: { + closeBrackets: { brackets: ["[", "{", '"'] }, + commentTokens: { line: "//", block: { open: "/*", close: "*/" } }, + indentOnInput: /^\s*[\}\]]$/, + }, +}; diff --git a/src/modules/editor/lib/languageResolver.ts b/src/modules/editor/lib/languageResolver.ts index d19b5d5a..f0c9f497 100644 --- a/src/modules/editor/lib/languageResolver.ts +++ b/src/modules/editor/lib/languageResolver.ts @@ -33,6 +33,7 @@ const loaders: Record = { go: () => import("@codemirror/lang-go").then((m) => m.go()), py: () => import("@codemirror/lang-python").then((m) => m.python()), json: () => import("@codemirror/lang-json").then((m) => m.json()), + jsonc: () => import("./jsonc").then((m) => m.jsonc), md: () => import("@codemirror/lang-markdown").then((m) => m.markdown()), markdown: () => import("@codemirror/lang-markdown").then((m) => m.markdown()),