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
- Open-source lightweight cross-platform AI-native terminal (ADE)
+Open-source lightweight cross-platform AI-native terminal (ADE)
-
+
-
+
---
@@ -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()),