feat(diff): expand syntax highlighting — mjs/cjs/mts/cts, CSS/SCSS/Less, HTML/XML/SVG/Vue, GraphQL, JSONC/JSON5

Ports dsh-file-trace v0.2.1 highlight coverage into the shared diff engine: JS/TS module suffixes share the js/ts keyword lists; CSS gains its own config (slash-star block comments threaded across lines, hyphenated property words via a wider word charset, 120+ properties/selectors/at-rules); markup files gain markup comments plus common tags/attributes; GraphQL and JSONC/JSON5 map in. Previously css/scss/less had a wrong hash-comment config and html was plain CONFIG_LANG, so both effectively rendered uncolored.
This commit is contained in:
lhh010
2026-09-01 23:05:26 +08:00
parent b7138cf4fd
commit 0142ac550b
4 changed files with 155 additions and 17 deletions
+1 -1
View File
@@ -44,7 +44,7 @@
- **🌐 内嵌浏览器**:多开网页 tab,后退 / 前进 / 刷新;内容运行在沙箱 iframe;外链默认按协议分流——HTTP 在侧边栏打开、HTTPS 走系统浏览器(设置页可分别调整) - **🌐 内嵌浏览器**:多开网页 tab,后退 / 前进 / 刷新;内容运行在沙箱 iframe;外链默认按协议分流——HTTP 在侧边栏打开、HTTPS 走系统浏览器(设置页可分别调整)
- **💻 真实终端**:xterm.js + node-pty 真实 shell,断线重连回放;可选为模型注入 `terminal_*` 工具 - **💻 真实终端**:xterm.js + node-pty 真实 shell,断线重连回放;可选为模型注入 `terminal_*` 工具
- **📂 模型侧边栏打开(可选)**:全局设置开启后注入 `sidebar_open` 工具——模型可主动在侧边栏打开文件 / 文件夹(树以该目录为根)/ HTTP(S) 网页 - **📂 模型侧边栏打开(可选)**:全局设置开启后注入 `sidebar_open` 工具——模型可主动在侧边栏打开文件 / 文件夹(树以该目录为根)/ HTTP(S) 网页
- **🌿 文件变动**:Git 视角(真 diff / 历史 / 暂存·提交·还原 / worktree·子仓库选择)与本轮文件视角(模型读 / 写 / 编辑实时追踪,按文件分组、按类型筛选)**双视角合一**;统一 diff 渲染(改蓝配对 + 行内字符级高亮 + 语法着色 + 上下文折叠),底部可拖拽预览面板,可一键展开为独立 diff tab(默认自由浮窗,可在设置改为面板下半 split);`.md` 操作(读 / 写 / 编辑)预览头部可切换**阅读模式**——经共享 MarkdownText 渲染 GFM 表格 / 任务列表 / 删除线 / 脚注 / 数学公式,本地图片自动改写为 `/sidebar/file` 媒体路由(与编辑器预览同一渲染栈) - **🌿 文件变动**:Git 视角(真 diff / 历史 / 暂存·提交·还原 / worktree·子仓库选择)与本轮文件视角(模型读 / 写 / 编辑实时追踪,按文件分组、按类型筛选)**双视角合一**;统一 diff 渲染(改蓝配对 + 行内字符级高亮 + 语法着色(含 mjs/cjs/mts/cts、CSS/SCSS/Less、HTML/XML/SVG/Vue、GraphQL、JSONC/JSON5)+ 上下文折叠),底部可拖拽预览面板,可一键展开为独立 diff tab(默认自由浮窗,可在设置改为面板下半 split);`.md` 操作(读 / 写 / 编辑)预览头部可切换**阅读模式**——经共享 MarkdownText 渲染 GFM 表格 / 任务列表 / 删除线 / 脚注 / 数学公式,本地图片自动改写为 `/sidebar/file` 媒体路由(与编辑器预览同一渲染栈)
- **🧩 后台任务页**:subagent 拓扑 + 后台任务(退出码 / 实时输出 / 强制终止) - **🧩 后台任务页**:subagent 拓扑 + 后台任务(退出码 / 实时输出 / 强制终止)
- **💬 侧边对话(beta)**:Codex 风格的侧边线程——继承主会话完整上下文(含进行中的回合与工具调用)独立运行,不进入主会话;线程内可持续追问,一键「保存为新会话」提升为顶层会话 - **💬 侧边对话(beta)**:Codex 风格的侧边线程——继承主会话完整上下文(含进行中的回合与工具调用)独立运行,不进入主会话;线程内可持续追问,一键「保存为新会话」提升为顶层会话
- **🪟 双工作台**:右侧栏 + 底部面板;拖 Tab 拆分 / 合并分栏(可跨面板),移动端自动合并全宽抽屉 - **🪟 双工作台**:右侧栏 + 底部面板;拖 Tab 拆分 / 合并分栏(可跨面板),移动端自动合并全宽抽屉
+1 -1
View File
@@ -44,7 +44,7 @@
- **🌐 Embedded Browser**: multiple web tabs with back / forward / refresh; content runs in a sandboxed iframe; external links are routed by protocol by default — HTTP opens in the sidebar, HTTPS goes to the system browser (both adjustable in settings) - **🌐 Embedded Browser**: multiple web tabs with back / forward / refresh; content runs in a sandboxed iframe; external links are routed by protocol by default — HTTP opens in the sidebar, HTTPS goes to the system browser (both adjustable in settings)
- **💻 Real Terminal**: xterm.js + node-pty real shell, reconnect with transcript replay; optionally injects `terminal_*` tools for the model - **💻 Real Terminal**: xterm.js + node-pty real shell, reconnect with transcript replay; optionally injects `terminal_*` tools for the model
- **📂 Model-driven sidebar opens (opt-in)**: with the global setting on, the `sidebar_open` tool lets the model actively open files / folders (tree rooted there) / HTTP(S) pages in the sidebar - **📂 Model-driven sidebar opens (opt-in)**: with the global setting on, the `sidebar_open` tool lets the model actively open files / folders (tree rooted there) / HTTP(S) pages in the sidebar
- **🌿 Changes**: one tab, two lenses — **Git** (real diff / history / stage·commit·revert / worktree & child-repo selection) and **This Session** (live tracking of every file the model reads / writes / edits, grouped by file with kind filters); a unified diff renderer (mod pairing + intra-line character highlights + syntax coloring + context folding), a draggable bottom preview pane, and one-click expansion into a dedicated diff tab; `.md` ops (read / write / edit) offer a **reading-mode** toggle in the preview header — the shared MarkdownText renders GFM tables / task lists / strikethrough / footnotes / math, with local images rewritten through the /sidebar/file media route (the same rendering stack as the editor preview) - **🌿 Changes**: one tab, two lenses — **Git** (real diff / history / stage·commit·revert / worktree & child-repo selection) and **This Session** (live tracking of every file the model reads / writes / edits, grouped by file with kind filters); a unified diff renderer (mod pairing + intra-line character highlights + syntax coloring incl. mjs/cjs/mts/cts, CSS/SCSS/Less, HTML/XML/SVG/Vue, GraphQL, JSONC/JSON5 + context folding), a draggable bottom preview pane, and one-click expansion into a dedicated diff tab; `.md` ops (read / write / edit) offer a **reading-mode** toggle in the preview header — the shared MarkdownText renders GFM tables / task lists / strikethrough / footnotes / math, with local images rewritten through the /sidebar/file media route (the same rendering stack as the editor preview)
- **🧩 Background Tasks**: agent topology + background tasks (exit codes / live output / force-kill) - **🧩 Background Tasks**: agent topology + background tasks (exit codes / live output / force-kill)
- **💬 Side Chat (beta)**: Codex-style side threads — the child inherits the parent's FULL context (completed turns + the pending question + the in-progress turn's assistant output and tool activity, honestly frozen as "interrupted") and runs independently without entering the main conversation; threads support continuous follow-ups (auto-resumed after a DSH restart) and one-click "Save as new session" promotion to a top-level session - **💬 Side Chat (beta)**: Codex-style side threads — the child inherits the parent's FULL context (completed turns + the pending question + the in-progress turn's assistant output and tool activity, honestly frozen as "interrupted") and runs independently without entering the main conversation; threads support continuous follow-ups (auto-resumed after a DSH restart) and one-click "Save as new session" promotion to a top-level session
- **🪟 Dual Workbench**: right sidebar + bottom panel; drag tabs to split / merge panes (cross-panel), mobile auto-merges into a full-width drawer - **🪟 Dual Workbench**: right sidebar + bottom panel; drag tabs to split / merge panes (cross-panel), mobile auto-merges into a full-width drawer
+81 -13
View File
@@ -132,24 +132,81 @@ const MD_LANG: LangConfig = {
wordBody: WORD, wordBody: WORD,
} }
/** Extension → language id, mirroring the read tool's hint table. */ /** JS keyword list, shared by js/jsx/mjs/cjs. */
const LANGS: Readonly<Record<string, LangConfig>> = { const JS_WORDS = `async await break case catch class const continue debugger default delete do else
ts: C_FAMILY(`abstract any as asserts async await boolean break case catch class const constructor export extends false finally for from function get if implements import in instanceof interface
let new null of return set static super switch this throw true try typeof undefined var void
while with yield`
/** TS keyword list, shared by ts/tsx/mts/cts. */
const TS_WORDS = `abstract any as asserts async await boolean break case catch class const constructor
continue debugger declare default delete do else enum export extends false finally for from continue debugger declare default delete do else enum export extends false finally for from
function get if implements import in infer instanceof interface is keyof let module namespace function get if implements import in infer instanceof interface is keyof let module namespace
never new null number object of override private protected public readonly return satisfies set never new null number object of override private protected public readonly return satisfies set
static string super switch symbol this throw true try type typeof undefined union unknown var static string super switch symbol this throw true try type typeof undefined union unknown var
void while with yield`), void while with yield`
/** CSS family: block comments delimited by slash-star, hyphenated property words. */
const CSS_LANG: LangConfig = {
lineComments: [],
blockComment: ['/*', '*/'],
strings: ['"', "'"],
keywords: kw(`media import charset keyframes font-face supports page namespace layer scope container
property value at-rule when and not only from to important
background background-color background-image background-position background-size
border border-radius border-color border-width border-style bottom box-shadow box-sizing
color content cursor clip clip-path clear display direction
flex flex-direction flex-wrap flex-flow flex-grow flex-shrink flex-basis
font font-family font-size font-style font-weight float fill
grid grid-area grid-template grid-template-columns grid-template-rows grid-gap gap
height left letter-spacing line-height list-style margin margin-top margin-right
margin-bottom margin-left max-height max-width min-height min-width opacity order
outline overflow padding padding-top padding-right padding-bottom padding-left
position pointer-events right rotate scale translate transform transform-origin
text-align text-decoration text-transform top transition transition-property
user-select vertical-align visibility white-space width word-break word-spacing z-index
align-items align-content align-self justify-content justify-items justify-self
aspect-ratio inset object-fit object-position resize scroll-behavior filter backdrop-filter
animation animation-name animation-duration animation-timing-function animation-delay
will-change
html body p a div span li ul ol table tr td th form input button label select textarea img
section header footer main nav article aside h1 h2 h3 h4 h5 h6 i b em strong small pre code
inherit initial unset auto none fixed absolute relative sticky static flex block inline
inline-block inline-flex grid hidden visible hidden bold normal inherit root var calc env`),
constants: kw('true false'),
macro: false,
wordStart: /[A-Za-z-]/u,
wordBody: /[A-Za-z0-9-]/u,
}
/** Markup family (HTML/XML/SVG/Vue): markup comments plus common tags. */
const MARKUP_LANG: LangConfig = {
lineComments: [],
blockComment: ['<!--', '-->'],
strings: ['"', "'"],
keywords: kw(`html head body title meta link script style template slot
div span p a img ul ol li table thead tbody tr th td form input button label select
option textarea header footer main nav section article aside figure figcaption
h1 h2 h3 h4 h5 h6 strong em b i u s small br hr pre code blockquote
svg path circle rect line polyline polygon g defs use text symbol
xml doctype class id href src type value name content charset async defer
vue component props setup script-style export import v-if v-for v-bind v-on`),
constants: new Set(),
macro: false,
wordStart: LETTER,
wordBody: WORD,
}
/** Extension → language id, mirroring the read tool's hint table. */
const LANGS: Readonly<Record<string, LangConfig>> = {
ts: C_FAMILY(TS_WORDS),
tsx: C_FAMILY(`abstract any as asserts async await boolean break case catch class const constructor tsx: C_FAMILY(`abstract any as asserts async await boolean break case catch class const constructor
continue declare default delete do else enum export extends false finally for from function get continue declare default delete do else enum export extends false finally for from function get
if implements import in infer instanceof interface is keyof let module namespace never new null if implements import in infer instanceof interface is keyof let module namespace never new null
number object of override private protected public readonly return satisfies set static string number object of override private protected public readonly return satisfies set static string
super switch symbol this throw true try type typeof undefined union unknown var void while with super switch symbol this throw true try type typeof undefined union unknown var void while with
yield`), yield`),
js: C_FAMILY(`async await break case catch class const continue debugger default delete do else js: C_FAMILY(JS_WORDS),
export extends false finally for from function get if implements import in instanceof interface
let new null of return set static super switch this throw true try typeof undefined var void
while with yield`),
jsx: C_FAMILY(`async await break case catch class const continue debugger default delete do else jsx: C_FAMILY(`async await break case catch class const continue debugger default delete do else
export extends false finally for from function get if implements import in instanceof interface export extends false finally for from function get if implements import in instanceof interface
let new null of return set static super switch this throw true try typeof undefined var void let new null of return set static super switch this throw true try typeof undefined var void
@@ -225,11 +282,22 @@ const LANGS: Readonly<Record<string, LangConfig>> = {
md: MD_LANG, md: MD_LANG,
markdown: MD_LANG, markdown: MD_LANG,
mdx: MD_LANG, mdx: MD_LANG,
html: CONFIG_LANG, mjs: C_FAMILY(JS_WORDS),
htm: CONFIG_LANG, cjs: C_FAMILY(JS_WORDS),
css: HASH_FAMILY(''), mts: C_FAMILY(TS_WORDS),
scss: HASH_FAMILY(''), cts: C_FAMILY(TS_WORDS),
less: HASH_FAMILY(''), jsonc: CONFIG_LANG,
json5: CONFIG_LANG,
html: MARKUP_LANG,
htm: MARKUP_LANG,
xml: MARKUP_LANG,
svg: MARKUP_LANG,
vue: MARKUP_LANG,
css: CSS_LANG,
scss: CSS_LANG,
less: CSS_LANG,
graphql: HASH_FAMILY('query mutation fragment on directive enum input interface scalar schema type implements'),
gql: HASH_FAMILY('query mutation fragment on directive enum input interface scalar schema type implements'),
lua: HASH_FAMILY(`and break do else elseif end false for function goto if in local nil not or lua: HASH_FAMILY(`and break do else elseif end false for function goto if in local nil not or
repeat return then true until while`), repeat return then true until while`),
} }
+70
View File
@@ -0,0 +1,70 @@
/**
* The highlight engine's v0.2.1-ported expansion: JS/TS module suffixes,
* CSS/SCSS/Less (block comments + hyphenated properties), markup files
* (comments + tags), and GraphQL mapping — the same coverage the standalone
* dsh-file-trace plugin ships.
*/
import { describe, expect, it } from 'vitest'
import { langOfPath, tokenizeLine, scanLine, hasBlockComment } from '../src/client/diff/highlight.ts'
describe('langOfPath: expanded suffixes', () => {
it('maps the JS/TS module suffixes to their families', () => {
expect(langOfPath('tool/config.mjs')).toBe('mjs')
expect(langOfPath('tool/config.cjs')).toBe('cjs')
expect(langOfPath('src/env.mts')).toBe('mts')
expect(langOfPath('src/env.cts')).toBe('cts')
})
it('maps markup, style and graphql suffixes', () => {
expect(langOfPath('site/index.html')).toBe('html')
expect(langOfPath('site/logo.svg')).toBe('svg')
expect(langOfPath('app/App.vue')).toBe('vue')
expect(langOfPath('data/feed.xml')).toBe('xml')
expect(langOfPath('style/main.css')).toBe('css')
expect(langOfPath('style/theme.scss')).toBe('scss')
expect(langOfPath('style/vars.less')).toBe('less')
expect(langOfPath('api/schema.graphql')).toBe('graphql')
expect(langOfPath('api/query.gql')).toBe('gql')
expect(langOfPath('cfg/settings.jsonc')).toBe('jsonc')
})
})
describe('tokenizeLine: CSS / markup', () => {
it('colors CSS block comments, hyphenated properties, strings and hex numbers', () => {
const toks = tokenizeLine('body { color: #333; background-color: var(--fg); content: "x" }', 'css')
expect(toks).toContainEqual(expect.objectContaining({ text: 'body', type: 'keyword' }))
expect(toks).toContainEqual(expect.objectContaining({ text: 'color', type: 'keyword' }))
expect(toks).toContainEqual(expect.objectContaining({ text: 'background-color', type: 'keyword' }))
expect(toks).toContainEqual(expect.objectContaining({ text: '"x"', type: 'string' }))
expect(toks).toContainEqual(expect.objectContaining({ text: '333', type: 'number' }))
const cmt = tokenizeLine('/* one line comment */ body', 'css')
expect(cmt[0]).toMatchObject({ text: '/* one line comment */', type: 'comment' })
})
it('threads CSS block comments across lines', () => {
expect(hasBlockComment('css')).toBe(true)
const open = scanLine('/* starts here', 'css')
expect(open.inBlock).toBe(true)
expect(open.tokens[0]).toMatchObject({ type: 'comment' })
const close = scanLine('ends here */ p {}', 'css', true)
expect(close.tokens[0]).toMatchObject({ type: 'comment' })
expect(close.tokens).toContainEqual(expect.objectContaining({ text: 'p', type: 'keyword' }))
})
it('colors markup comments, tags and attributes', () => {
const toks = tokenizeLine('<div class="box">hi</div>', 'html')
expect(toks).toContainEqual(expect.objectContaining({ text: 'div', type: 'keyword' }))
expect(toks).toContainEqual(expect.objectContaining({ text: 'class', type: 'keyword' }))
expect(toks).toContainEqual(expect.objectContaining({ text: '"box"', type: 'string' }))
const cmt = tokenizeLine('<!-- header -->', 'html')
expect(cmt[0]).toMatchObject({ text: '<!-- header -->', type: 'comment' })
expect(hasBlockComment('html')).toBe(true)
})
it('colors mjs like js (await keyword wins over the call-site rule)', () => {
const toks = tokenizeLine('const x = await import("./mod.js")', 'mjs')
expect(toks).toContainEqual(expect.objectContaining({ text: 'const', type: 'keyword' }))
expect(toks).toContainEqual(expect.objectContaining({ text: 'await', type: 'keyword' }))
expect(toks).toContainEqual(expect.objectContaining({ text: 'import', type: 'keyword' }))
})
})