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:
@@ -44,7 +44,7 @@
|
||||
- **🌐 内嵌浏览器**:多开网页 tab,后退 / 前进 / 刷新;内容运行在沙箱 iframe;外链默认按协议分流——HTTP 在侧边栏打开、HTTPS 走系统浏览器(设置页可分别调整)
|
||||
- **💻 真实终端**:xterm.js + node-pty 真实 shell,断线重连回放;可选为模型注入 `terminal_*` 工具
|
||||
- **📂 模型侧边栏打开(可选)**:全局设置开启后注入 `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 拓扑 + 后台任务(退出码 / 实时输出 / 强制终止)
|
||||
- **💬 侧边对话(beta)**:Codex 风格的侧边线程——继承主会话完整上下文(含进行中的回合与工具调用)独立运行,不进入主会话;线程内可持续追问,一键「保存为新会话」提升为顶层会话
|
||||
- **🪟 双工作台**:右侧栏 + 底部面板;拖 Tab 拆分 / 合并分栏(可跨面板),移动端自动合并全宽抽屉
|
||||
|
||||
+1
-1
@@ -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)
|
||||
- **💻 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
|
||||
- **🌿 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)
|
||||
- **💬 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
|
||||
|
||||
@@ -132,24 +132,81 @@ const MD_LANG: LangConfig = {
|
||||
wordBody: WORD,
|
||||
}
|
||||
|
||||
/** JS keyword list, shared by js/jsx/mjs/cjs. */
|
||||
const JS_WORDS = `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
|
||||
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
|
||||
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
|
||||
static string super switch symbol this throw true try type typeof undefined union unknown var
|
||||
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(`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
|
||||
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
|
||||
static string super switch symbol this throw true try type typeof undefined union unknown var
|
||||
void while with yield`),
|
||||
ts: C_FAMILY(TS_WORDS),
|
||||
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
|
||||
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
|
||||
super switch symbol this throw true try type typeof undefined union unknown var void while with
|
||||
yield`),
|
||||
js: 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
|
||||
let new null of return set static super switch this throw true try typeof undefined var void
|
||||
while with yield`),
|
||||
js: C_FAMILY(JS_WORDS),
|
||||
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
|
||||
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,
|
||||
markdown: MD_LANG,
|
||||
mdx: MD_LANG,
|
||||
html: CONFIG_LANG,
|
||||
htm: CONFIG_LANG,
|
||||
css: HASH_FAMILY(''),
|
||||
scss: HASH_FAMILY(''),
|
||||
less: HASH_FAMILY(''),
|
||||
mjs: C_FAMILY(JS_WORDS),
|
||||
cjs: C_FAMILY(JS_WORDS),
|
||||
mts: C_FAMILY(TS_WORDS),
|
||||
cts: C_FAMILY(TS_WORDS),
|
||||
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
|
||||
repeat return then true until while`),
|
||||
}
|
||||
|
||||
@@ -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' }))
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user