@rooode/dsh-plugin-preview 0.1.4 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,46 +1,54 @@
1
- # DeepSeek Harness - 右侧 Markdown 文档预览插件 (`@rooode/dsh-plugin-preview`)
1
+ # DeepSeek Harness - 多功能文档与工作空间代码预览插件 (`@rooode/dsh-plugin-preview`)
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/@rooode/dsh-plugin-preview.svg?style=flat-square)](https://www.npmjs.com/package/@rooode/dsh-plugin-preview)
4
4
  [![license](https://img.shields.io/npm/l/@rooode/dsh-plugin-preview.svg?style=flat-square)](https://github.com/rooode)
5
5
 
6
- 本插件为 **DeepSeek Harness (DSH)** 与 **WorkBuddy** 对话界面提供了参考 WorkBuddy `FileTabs` 设计的**右侧多标签 Markdown 与文档预览面板**。
6
+ 本插件为 **DeepSeek Harness (DSH)** 与 **WorkBuddy** 对话界面提供了参考 WorkBuddy `FileTabs` 设计的**右侧多标签 Markdown、代码与工作空间文件树浏览器**。
7
7
 
8
- 在 DSH 中,大模型经常会生成、改写或读取 `.md` 文档。默认情况下,点击生成的文档链接会直接调用 Windows 系统的本地记事本 (Notepad) 打开,打断了用户在 Web 会话中的阅读与操作体验。通过安装本插件,点击任何 Markdown 文档将直接在**右侧滑出多标签预览抽屉**,提供富文本渲染、目录大纲、源码分栏、一键复制、实时刷新以及按需本地打开等全套能力。
8
+ 在 DSH 中,大模型经常会生成、改写或读取 `.md` 文档与各类代码文件。默认情况下,点击生成的文档链接会直接调用 Windows 系统的本地记事本 (Notepad) 打开,打断了用户在 Web 会话中的阅读与操作体验。通过安装本插件,点击任何文档或代码将直接在**右侧滑出多标签预览抽屉**,提供工作空间目录树浏览、多语言代码高亮与符号大纲、JSON 交互结构树、GFM 富文本渲染、一键复制、实时刷新以及按需本地打开等全套能力。
9
9
 
10
10
  ---
11
11
 
12
12
  ## 🌟 核心特性 (Features)
13
13
 
14
- 1. **自动拦截与无感打开**:
15
- - 自动拦截对话底部 **产出文件行 (`ProducedFiles`)**、**工具执行行 (`ToolRow`)** 以及对话正文中的所有 `.md`、`.markdown`、`.txt`、`.json` 等文档点击。
16
- - 不再弹出本地记事本,直接在右侧平滑滑出预览面板。
17
-
18
- 2. **右侧多标签页支持 (FileTabs)**:
19
- - 支持同时打开多个文档,标签栏清晰展示文件图标、文档名、修改标记与独立关闭按钮。
20
- - 支持快速切换激活标签、一键全部关闭。
21
- - 左侧边框支持 **鼠标自由拖拽调节宽度 (ResizeHandle)**(支持 380px 至 90vw 自由调整)。
22
-
23
- 3. **全功能 GFM Markdown 渲染引擎**:
24
- - **三模视图切换**:
25
- - 📖 **渲染预览 (Preview)**:排版精美的富文本 Markdown 渲染。
26
- - 📝 **源码模式 (Source)**:带行号与等宽字体的 Markdown 原文。
27
- - 🌓 **分栏对比 (Split)**:左右双栏并排对照。
14
+ 1. **📁 工作空间目录树浏览器 (Workspace Tree Explorer)**:
15
+ - 树状层级展开/折叠项目所有目录与文件,支持文件大小与修改时间展示。
16
+ - 文件名即时过滤搜索,自动展开命中路径并折叠无关项。
17
+ - 快捷折叠全部目录、一键刷新磁盘目录结构。
18
+ - 目录树宽度支持鼠标自由拖拽调节。
19
+
20
+ 2. **💡 对话区智能交互与自动收起**:
21
+ - 当预览抽屉处于打开状态时,点击左侧会话列表或对话输入框,抽屉自动平滑收起/隐藏,保证专注对话输入。
22
+ - 收起后右下角保留胶囊浮窗,一键随时恢复展开。
23
+
24
+ 3. **🌳 交互式 JSON 结构树检查器 (JSON Tree Inspector)**:
25
+ - `.json` 文件默认以交互结构树呈现,支持 `Object{N}` `Array[N]` 多层级折叠展开。
26
+ - 属性名与属性值即时过滤搜索。
27
+ - 单击快捷复制指定字段路径(如 `scripts.build`)或字段值。
28
+ - 提供 `⚡ 格式化 (2空格对齐)` 与 `🗜️ 压缩 (单行)` 快捷操作,并支持随时切换至高亮代码模式。
29
+
30
+ 4. **🌐 多语言语法高亮与符号大纲 (Polyglot Highlighting & Symbol TOC)**:
31
+ - 采用单趟扫描分词器(Single-Pass Tokenizer),杜绝 HTML 嵌套污染。
32
+ - **全面支持主流语言**:
33
+ - ☕ **Java**:注解(`@Service`, `@Autowired`, `@Override`)、泛型、集合类、文本块、Package/Class/Method 符号大纲。
34
+ - ⚙️ **C / C++**:预处理指令(`#include <...>`)、命名空间、模板、标准库类型、智能指针、Namespace/Class/Struct/Function 符号大纲。
35
+ - 🐍 **Python**:装饰器(`@dataclass`, `@property`)、多行 Docstrings、异步协程(`async def`)、类型标注、Class/Def 符号大纲。
36
+ - ⚡ **JavaScript / TypeScript**:ES 模块、JSX 标签、接口/枚举、模板字符串、箭头函数、Class/Fn/Type/Export 符号大纲。
37
+ - 🦀 **Rust** / 🌐 **Go** / ⌨️ **Shell** / 🗄️ **SQL** / ⚙️ **YAML** / 📝 **Markdown** 等。
38
+ - **大纲点击平滑跳转**:点击大纲侧栏任一项,代码区域自动平滑滚动定位至目标代码行,并在行号及对应行触发高亮闪烁反馈。
39
+ - **代码排版优化**:默认水平滚动,点击 `↩️ 换行` 可在单行与自动折行(`overflow-wrap: break-word`)间无缝切换,杜绝单词被撕裂打碎。
40
+
41
+ 5. **📖 全功能 GFM Markdown 渲染引擎**:
42
+ - **三模视图切换**:📖 渲染预览 (Preview) / 📝 源码模式 (Source) / 🌓 分栏对比 (Split)。
28
43
  - **GitHub Alerts 提示框**:完整支持 `> [!NOTE]`、`> [!TIP]`、`> [!IMPORTANT]`、`> [!WARNING]`、`> [!CAUTION]` 彩色卡片渲染。
29
- - **代码块语法高亮**:支持 JavaScript/TypeScript、Python、JSON、Bash、HTML、CSS 等多种语言关键字高亮,顶部附带语言标识与**一键复制代码**按钮。
30
44
  - **LaTeX 数学公式**:支持 `$inline$` 与 `$$block$$` 数学公式排版。
31
45
  - **数据表格与任务清单**:斑马纹表格自适应排版、交互式勾选框列表。
32
- - **目录大纲导航 (TOC)**:自动提取 H1~H6 标题生成侧栏目录,点击平滑滚动跳转至对应章节。
46
+ - **目录大纲导航 (TOC)**:自动提取 H1~H6 标题生成侧栏目录,点击平滑滚动跳转。
33
47
 
34
- 4. **实用操作工具栏**:
35
- - 📋 **一键复制 (Copy)**:一键复制完整 Markdown 内容至系统剪贴板。
36
- - 🔄 **实时刷新 (Refresh)**:重新从磁盘加载最新文件内容(Agent 持续写入时可一键更新)。
37
- - 🖥️ **本地应用打开 (Open Native)**:若确需在系统外部编辑器(记事本/VSCode)中打开,可一键唤起。
38
- - 📁 **资源管理器中定位 (Reveal)**:一键调用系统资源管理器定位并高亮选中该文件。
39
- - 🔍 **文档内关键词搜索 (Find)**:快速高亮匹配文本。
40
- - ✕ **快捷收起与悬浮入口**:支持 Esc 快捷键关闭;收起后右下角保留胶囊浮窗,一键随时恢复展开。
41
-
42
- 5. **Node 端服务与安全保障**:
43
- - 配合 Node 端 Cordis `PreviewService`,提供 `/api/preview/read`、`/api/preview/reveal` 等 REST 路由,解决本地工作区文件跨目录读取与超大文件防护。
48
+ 6. **📑 右侧多标签页 (FileTabs)**:
49
+ - 支持同时打开多个文档与代码文件,标签栏展示对应语言彩色图标、文件名与独立关闭按钮。
50
+ - 支持快速切换激活标签、一键全部关闭。
51
+ - 左侧边框支持鼠标自由拖拽调节宽度(支持 380px 90vw 自由调整)。
44
52
 
45
53
  ---
46
54
 
@@ -67,32 +75,9 @@ npx @deepseek-ai/dsh web
67
75
 
68
76
  ---
69
77
 
70
- ## 📁 目录结构 (Directory Structure)
71
-
72
- ```text
73
- dsh-plugin-preview/
74
- ├── package.json # 插件元信息与 npm 脚本配置
75
- ├── cordis.patch.yml # DSH 组合包 patch 描述文件
76
- ├── tsconfig.json # TypeScript 编译配置
77
- ├── README.md # 插件说明文档
78
- ├── scripts/
79
- │ └── build.js # 自动构建、分发与补丁安装脚本
80
- ├── src/
81
- │ ├── types.ts # 核心 TypeScript 类型定义
82
- │ ├── service.ts # Node 端 PreviewService 服务 (HTTP 路由)
83
- │ ├── index.ts # Node 端 Cordis Service 注册与 Schema 导出
84
- │ └── client.js # Web 浏览器端 React 预览面板与拦截器
85
- └── lib/
86
- ├── index.js # Node ESM 导出产物
87
- ├── index.d.ts # 类型声明文件
88
- └── client.js # Web 端 ModuleLoader 加载产物
89
- ```
90
-
91
- ---
92
-
93
78
  ## ⚙️ 配置项说明 (Configuration)
94
79
 
95
- 可以在 `cordis.patch.yml` 或配置中自定义以下参数:
80
+ 可以在 `cordis.patch.yml` 或配置文件中自定义以下参数:
96
81
 
97
82
  ```yaml
98
83
  - id: ui-preview
@@ -106,12 +91,24 @@ dsh-plugin-preview/
106
91
  - .json
107
92
  - .yaml
108
93
  - .yml
94
+ - .js
95
+ - .ts
96
+ - .java
97
+ - .cpp
98
+ - .c
99
+ - .h
100
+ - .hpp
101
+ - .py
102
+ - .go
103
+ - .rs
104
+ - .sh
105
+ - .sql
109
106
  # 最大允许读取的文件大小 (MB)
110
107
  maxFileSizeMb: 10
111
- # 默认打开视图: 'preview' (预览) | 'source' (源码) | 'split' (分栏)
108
+ # 默认打开视图: 'preview' (预览) | 'source' (源码) | 'split' (分栏) | 'json-tree' (树) | 'code' (代码)
112
109
  defaultViewMode: preview
113
110
  # 右侧预览面板默认宽度 (px)
114
- defaultPanelWidth: 620
111
+ defaultPanelWidth: 760
115
112
  ```
116
113
 
117
114
  ---
package/cordis.patch.yml CHANGED
@@ -13,4 +13,16 @@
13
13
  - .json
14
14
  - .yaml
15
15
  - .yml
16
+ - .js
17
+ - .ts
18
+ - .java
19
+ - .cpp
20
+ - .c
21
+ - .h
22
+ - .hpp
23
+ - .py
24
+ - .go
25
+ - .rs
26
+ - .sh
27
+ - .sql
16
28
  maxFileSizeMb: 10
package/lib/client.js CHANGED
@@ -33,8 +33,13 @@
33
33
  const SUPPORTED_EXTENSIONS = new Set([
34
34
  '.md', '.markdown', '.mdown', '.mkdn', '.mdwn',
35
35
  '.txt', '.log', '.json', '.yaml', '.yml',
36
- '.js', '.jsx', '.ts', '.tsx', '.html', '.css', '.scss',
37
- '.py', '.sh', '.bash', '.ps1', '.sql', '.toml', '.xml'
36
+ '.js', '.jsx', '.ts', '.tsx', '.html', '.css', '.scss', '.less',
37
+ '.java', '.jav', '.jsp',
38
+ '.cpp', '.c', '.cc', '.cxx', '.h', '.hpp', '.hxx', '.inl',
39
+ '.py', '.pyw', '.ipynb',
40
+ '.go', '.rs', '.cs', '.kt',
41
+ '.sh', '.bash', '.zsh', '.ps1', '.bat', '.cmd',
42
+ '.sql', '.toml', '.xml', '.svg', '.ini', '.env', '.properties'
38
43
  ]);
39
44
 
40
45
  // =========================================================================
@@ -172,8 +177,15 @@
172
177
  if (ext === '.yaml' || ext === '.yml' || ext === '.toml' || ext === '.ini') return { icon: '⚙️', color: '#f97316' };
173
178
  if (ext === '.html' || ext === '.htm') return { icon: '🌐', color: '#ef4444' };
174
179
  if (ext === '.css' || ext === '.scss' || ext === '.less') return { icon: '#', color: '#06b6d4' };
175
- if (ext === '.py') return { icon: '🐍', color: '#38bdf8' };
176
- if (ext === '.sh' || ext === '.bash' || ext === '.ps1') return { icon: '⌨️', color: '#8b5cf6' };
180
+ if (ext === '.java' || ext === '.jav') return { icon: '', color: '#ea580c' };
181
+ if (['.cpp', '.cc', '.cxx', '.hpp', '.hxx'].includes(ext)) return { icon: 'C++', color: '#0284c7' };
182
+ if (ext === '.c' || ext === '.h') return { icon: 'C', color: '#64748b' };
183
+ if (ext === '.py' || ext === '.pyw') return { icon: '🐍', color: '#38bdf8' };
184
+ if (ext === '.go') return { icon: 'GO', color: '#00add8' };
185
+ if (ext === '.rs') return { icon: '🦀', color: '#ea580c' };
186
+ if (ext === '.cs') return { icon: 'C#', color: '#a855f7' };
187
+ if (ext === '.kt') return { icon: 'KT', color: '#7c3aed' };
188
+ if (ext === '.sh' || ext === '.bash' || ext === '.ps1' || ext === '.zsh') return { icon: '⌨️', color: '#8b5cf6' };
177
189
  if (['.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp'].includes(ext)) return { icon: '🖼️', color: '#ec4899' };
178
190
  if (ext === '.log' || ext === '.txt') return { icon: '📄', color: '#64748b' };
179
191
  return { icon: '📄', color: '#94a3b8' };
@@ -187,7 +199,12 @@
187
199
  if (['.json', '.jsonc', '.json5', '.geojson', '.lock'].includes(ext)) return 'json';
188
200
  if (['.yaml', '.yml'].includes(ext)) return 'yaml';
189
201
  if (['.js', '.jsx', '.mjs', '.cjs', '.ts', '.tsx', '.mts', '.cts'].includes(ext)) return 'javascript';
190
- if (['.py', '.pyw'].includes(ext)) return 'python';
202
+ if (['.java', '.jav', '.jsp'].includes(ext)) return 'java';
203
+ if (['.cpp', '.c', '.cc', '.cxx', '.h', '.hpp', '.hxx', '.inl'].includes(ext)) return 'cpp';
204
+ if (['.py', '.pyw', '.ipynb'].includes(ext)) return 'python';
205
+ if (['.go'].includes(ext)) return 'go';
206
+ if (['.rs'].includes(ext)) return 'rust';
207
+ if (['.cs'].includes(ext)) return 'csharp';
191
208
  if (['.html', '.htm', '.svg', '.xml'].includes(ext)) return 'markup';
192
209
  if (['.css', '.scss', '.less', '.sass'].includes(ext)) return 'css';
193
210
  if (['.sh', '.bash', '.zsh', '.ps1', '.bat', '.cmd'].includes(ext)) return 'shell';
@@ -1239,19 +1256,172 @@
1239
1256
  return out;
1240
1257
  }
1241
1258
 
1259
+ function highlightJava(code) {
1260
+ if (!code) return '';
1261
+ const keywords = new Set([
1262
+ 'public', 'private', 'protected', 'class', 'interface', 'enum', 'record', 'extends',
1263
+ 'implements', 'static', 'final', 'abstract', 'void', 'return', 'if', 'else', 'for',
1264
+ 'while', 'do', 'switch', 'case', 'break', 'continue', 'default', 'new', 'this', 'super',
1265
+ 'try', 'catch', 'finally', 'throw', 'throws', 'import', 'package', 'synchronized',
1266
+ 'volatile', 'transient', 'native', 'strictfp', 'instanceof', 'assert', 'yield', 'sealed',
1267
+ 'permits', 'var', 'const', 'goto'
1268
+ ]);
1269
+ const types = new Set([
1270
+ 'int', 'long', 'short', 'byte', 'float', 'double', 'boolean', 'char', 'void',
1271
+ 'String', 'Object', 'Integer', 'Long', 'Boolean', 'Double', 'Float', 'Byte', 'Short', 'Character',
1272
+ 'List', 'Map', 'Set', 'ArrayList', 'HashMap', 'HashSet', 'LinkedList', 'TreeMap', 'TreeSet',
1273
+ 'Optional', 'Stream', 'Arrays', 'Collections', 'System', 'Thread', 'Exception', 'RuntimeException',
1274
+ 'Throwable', 'Error', 'Class', 'StringBuilder', 'StringBuffer', 'CompletableFuture', 'Future',
1275
+ 'null', 'true', 'false'
1276
+ ]);
1277
+
1278
+ const tokenRegex = /(\/\*[\s\S]*?\*\/|\/\/.*$|"""[\s\S]*?"""|"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|@[a-zA-Z0-9_.]+|\b(?:0x[0-9a-fA-F]+|\d+(?:\.\d+)?(?:[eE][+\-]?\d+)?[fFdDlL]?)\b|\b[a-zA-Z_$][a-zA-Z0-9_$]*\b|->|::|==|!=|<=|>=|&&|\|\||\+\+|--|\+=|-=|\*=|\/=|%=|&=|\|=|\^=|>>>=|>>=|<<=|[+\-*/%=!&|^~<>?:;,{}()\[\].])/gm;
1279
+
1280
+ let lastIndex = 0;
1281
+ let out = '';
1282
+ let match;
1283
+
1284
+ while ((match = tokenRegex.exec(code)) !== null) {
1285
+ const raw = match[0];
1286
+ const index = match.index;
1287
+ if (index > lastIndex) {
1288
+ out += escapeHtml(code.slice(lastIndex, index));
1289
+ }
1290
+ lastIndex = tokenRegex.lastIndex;
1291
+
1292
+ if (raw.startsWith('/*') || raw.startsWith('//')) {
1293
+ out += '<span class="tok-comment">' + escapeHtml(raw) + '</span>';
1294
+ } else if (raw.startsWith('"""') || raw.startsWith('"') || raw.startsWith("'")) {
1295
+ out += '<span class="tok-string">' + escapeHtml(raw) + '</span>';
1296
+ } else if (raw.startsWith('@')) {
1297
+ out += '<span class="tok-attr">' + escapeHtml(raw) + '</span>';
1298
+ } else if (/^\d|^0x/.test(raw)) {
1299
+ out += '<span class="tok-number">' + escapeHtml(raw) + '</span>';
1300
+ } else if (/^[a-zA-Z_$]/.test(raw)) {
1301
+ if (keywords.has(raw)) {
1302
+ out += '<span class="tok-keyword">' + escapeHtml(raw) + '</span>';
1303
+ } else if (types.has(raw)) {
1304
+ out += '<span class="tok-type">' + escapeHtml(raw) + '</span>';
1305
+ } else {
1306
+ const remainder = code.slice(lastIndex);
1307
+ if (/^\s*\(/.test(remainder)) {
1308
+ out += '<span class="tok-func">' + escapeHtml(raw) + '</span>';
1309
+ } else {
1310
+ out += escapeHtml(raw);
1311
+ }
1312
+ }
1313
+ } else if (/^(->|::|==|!=|<=|>=|&&|\|\||\+\+|--|\+=|-=|\*=|\/=|%=|&=|\|=|\^=|>>>=|>>=|<<=|[+\-*/%=!&|^~<>])$/.test(raw)) {
1314
+ out += '<span class="tok-operator">' + escapeHtml(raw) + '</span>';
1315
+ } else if (/^[{}()\[\]]$/.test(raw)) {
1316
+ out += '<span class="tok-bracket">' + escapeHtml(raw) + '</span>';
1317
+ } else if (/^[,;:?]$/.test(raw)) {
1318
+ out += '<span class="tok-punctuation">' + escapeHtml(raw) + '</span>';
1319
+ } else {
1320
+ out += escapeHtml(raw);
1321
+ }
1322
+ }
1323
+
1324
+ if (lastIndex < code.length) {
1325
+ out += escapeHtml(code.slice(lastIndex));
1326
+ }
1327
+ return out;
1328
+ }
1329
+
1330
+ function highlightCpp(code) {
1331
+ if (!code) return '';
1332
+ const keywords = new Set([
1333
+ 'class', 'struct', 'union', 'enum', 'template', 'typename', 'namespace', 'using',
1334
+ 'typedef', 'public', 'private', 'protected', 'virtual', 'override', 'final', 'const',
1335
+ 'constexpr', 'consteval', 'constinit', 'static', 'inline', 'auto', 'void', 'return',
1336
+ 'if', 'else', 'for', 'while', 'do', 'switch', 'case', 'break', 'continue', 'default',
1337
+ 'new', 'delete', 'this', 'try', 'catch', 'throw', 'noexcept', 'explicit', 'friend',
1338
+ 'mutable', 'volatile', 'concept', 'requires', 'co_await', 'co_yield', 'co_return',
1339
+ 'decltype', 'sizeof', 'alignas', 'alignof', 'static_cast', 'dynamic_cast',
1340
+ 'reinterpret_cast', 'const_cast', 'operator', 'export', 'import', 'module'
1341
+ ]);
1342
+ const types = new Set([
1343
+ 'int', 'long', 'short', 'char', 'float', 'double', 'bool', 'void', 'wchar_t', 'char8_t',
1344
+ 'char16_t', 'char32_t', 'size_t', 'int8_t', 'int16_t', 'int32_t', 'int64_t',
1345
+ 'uint8_t', 'uint16_t', 'uint32_t', 'uint64_t', 'nullptr', 'true', 'false',
1346
+ 'string', 'wstring', 'vector', 'map', 'unordered_map', 'set', 'unordered_set',
1347
+ 'pair', 'tuple', 'unique_ptr', 'shared_ptr', 'weak_ptr', 'make_unique', 'make_shared',
1348
+ 'std', 'cout', 'cin', 'cerr', 'endl', 'array', 'deque', 'list', 'queue', 'stack'
1349
+ ]);
1350
+
1351
+ const tokenRegex = /(\/\*[\s\S]*?\*\/|\/\/.*$|#(?:include|define|ifdef|ifndef|endif|if|else|elif|pragma|undef|error)\b.*$|R"\(.*?\)"|"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|\b(?:0x[0-9a-fA-F]+|\d+(?:\.\d+)?(?:[eE][+\-]?\d+)?[uUlLfF]?)\b|\b[a-zA-Z_][a-zA-Z0-9_]*\b|::|->\*|->|==|!=|<=|>=|<=>|&&|\|\||\+\+|--|\+=|-=|\*=|\/=|%=|&=|\|=|\^=|>>=|<<=|<<|>>|[+\-*/%=!&|^~<>?:;,{}()\[\].])/gm;
1352
+
1353
+ let lastIndex = 0;
1354
+ let out = '';
1355
+ let match;
1356
+
1357
+ while ((match = tokenRegex.exec(code)) !== null) {
1358
+ const raw = match[0];
1359
+ const index = match.index;
1360
+ if (index > lastIndex) {
1361
+ out += escapeHtml(code.slice(lastIndex, index));
1362
+ }
1363
+ lastIndex = tokenRegex.lastIndex;
1364
+
1365
+ if (raw.startsWith('/*') || raw.startsWith('//')) {
1366
+ out += '<span class="tok-comment">' + escapeHtml(raw) + '</span>';
1367
+ } else if (raw.startsWith('#')) {
1368
+ const spaceIdx = raw.indexOf(' ');
1369
+ if (spaceIdx !== -1) {
1370
+ const dir = raw.slice(0, spaceIdx);
1371
+ const rest = raw.slice(spaceIdx);
1372
+ out += '<span class="tok-keyword">' + escapeHtml(dir) + '</span><span class="tok-string">' + escapeHtml(rest) + '</span>';
1373
+ } else {
1374
+ out += '<span class="tok-keyword">' + escapeHtml(raw) + '</span>';
1375
+ }
1376
+ } else if (raw.startsWith('R"') || raw.startsWith('"') || raw.startsWith("'")) {
1377
+ out += '<span class="tok-string">' + escapeHtml(raw) + '</span>';
1378
+ } else if (/^\d|^0x/.test(raw)) {
1379
+ out += '<span class="tok-number">' + escapeHtml(raw) + '</span>';
1380
+ } else if (/^[a-zA-Z_]/.test(raw)) {
1381
+ if (keywords.has(raw)) {
1382
+ out += '<span class="tok-keyword">' + escapeHtml(raw) + '</span>';
1383
+ } else if (types.has(raw)) {
1384
+ out += '<span class="tok-type">' + escapeHtml(raw) + '</span>';
1385
+ } else {
1386
+ const remainder = code.slice(lastIndex);
1387
+ if (/^\s*\(/.test(remainder)) {
1388
+ out += '<span class="tok-func">' + escapeHtml(raw) + '</span>';
1389
+ } else {
1390
+ out += escapeHtml(raw);
1391
+ }
1392
+ }
1393
+ } else if (/^(::|->\*|->|==|!=|<=|>=|<=>|&&|\|\||\+\+|--|\+=|-=|\*=|\/=|%=|&=|\|=|\^=|>>=|<<=|<<|>>|[+\-*/%=!&|^~<>])$/.test(raw)) {
1394
+ out += '<span class="tok-operator">' + escapeHtml(raw) + '</span>';
1395
+ } else if (/^[{}()\[\]]$/.test(raw)) {
1396
+ out += '<span class="tok-bracket">' + escapeHtml(raw) + '</span>';
1397
+ } else if (/^[,;:?]$/.test(raw)) {
1398
+ out += '<span class="tok-punctuation">' + escapeHtml(raw) + '</span>';
1399
+ } else {
1400
+ out += escapeHtml(raw);
1401
+ }
1402
+ }
1403
+
1404
+ if (lastIndex < code.length) {
1405
+ out += escapeHtml(code.slice(lastIndex));
1406
+ }
1407
+ return out;
1408
+ }
1409
+
1242
1410
  function highlightPython(code) {
1243
1411
  if (!code) return '';
1244
1412
  const pyKeywords = new Set([
1245
1413
  'def', 'class', 'import', 'from', 'return', 'if', 'elif', 'else', 'for', 'while',
1246
1414
  'try', 'except', 'finally', 'raise', 'with', 'as', 'lambda', 'pass', 'yield',
1247
- 'async', 'await', 'global', 'nonlocal', 'assert', 'del', 'in', 'is', 'not', 'and', 'or'
1415
+ 'async', 'await', 'global', 'nonlocal', 'assert', 'del', 'in', 'is', 'not', 'and', 'or',
1416
+ 'match', 'case'
1248
1417
  ]);
1249
1418
  const pyBuiltins = new Set([
1250
1419
  'True', 'False', 'None', 'self', 'cls', 'print', 'len', 'range', 'dict', 'list',
1251
- 'set', 'tuple', 'str', 'int', 'float', 'bool', 'type', 'isinstance', 'enumerate', 'zip'
1420
+ 'set', 'tuple', 'str', 'int', 'float', 'bool', 'type', 'isinstance', 'enumerate', 'zip',
1421
+ 'super', 'sum', 'min', 'max', 'any', 'all', 'open', 'map', 'filter'
1252
1422
  ]);
1253
1423
 
1254
- const tokenRegex = /("""[\s\S]*?"""|'''[\s\S]*?'''|#.*$|f?"(?:\\[\s\S]|[^"\\])*"|f?'(?:\\[\s\S]|[^'\\])*'|\b\d+(?:\.\d+)?\b|\b[a-zA-Z_][a-zA-Z0-9_]*\b|==|!=|<=|>=|\+=|-=|\*=|\/=|->|[+\-*/%=&|^~<>?:;,{}()\[\].])/gm;
1424
+ const tokenRegex = /("""[\s\S]*?"""|'''[\s\S]*?'''|#.*$|@[a-zA-Z0-9_.]+|f?"(?:\\[\s\S]|[^"\\])*"|f?'(?:\\[\s\S]|[^'\\])*'|\b\d+(?:\.\d+)?\b|\b[a-zA-Z_][a-zA-Z0-9_]*\b|==|!=|<=|>=|\+=|-=|\*=|\/=|->|[+\-*/%=&|^~<>?:;,{}()\[\].])/gm;
1255
1425
 
1256
1426
  let lastIndex = 0;
1257
1427
  let out = '';
@@ -1269,6 +1439,8 @@
1269
1439
  out += '<span class="tok-comment">' + escapeHtml(raw) + '</span>';
1270
1440
  } else if (raw.startsWith('"""') || raw.startsWith("'''") || raw.startsWith('"') || raw.startsWith("'") || raw.startsWith('f"') || raw.startsWith("f'")) {
1271
1441
  out += '<span class="tok-string">' + escapeHtml(raw) + '</span>';
1442
+ } else if (raw.startsWith('@')) {
1443
+ out += '<span class="tok-attr">' + escapeHtml(raw) + '</span>';
1272
1444
  } else if (/^\d/.test(raw)) {
1273
1445
  out += '<span class="tok-number">' + escapeHtml(raw) + '</span>';
1274
1446
  } else if (/^[a-zA-Z_]/.test(raw)) {
@@ -1347,6 +1519,22 @@
1347
1519
  .replace(/(--.*$)/gm, '<span class="tok-comment">$1</span>')
1348
1520
  .replace(/('[^']*')/g, '<span class="tok-string">$1</span>');
1349
1521
  }
1522
+ if (category === 'go') {
1523
+ const goKeywords = /\b(package|import|func|return|var|const|type|struct|interface|map|chan|go|defer|if|else|for|range|switch|case|default|select|break|continue|fallthrough)\b/g;
1524
+ return escapeHtml(code)
1525
+ .replace(/(\b(?:string|int|int64|int32|uint|uint64|byte|rune|bool|float32|float64|error|nil|true|false)\b)/g, '<span class="tok-type">$1</span>')
1526
+ .replace(goKeywords, '<span class="tok-keyword">$1</span>')
1527
+ .replace(/(\/\/.*$)/gm, '<span class="tok-comment">$1</span>')
1528
+ .replace(/("(?:\\[\s\S]|[^"\\])*")/g, '<span class="tok-string">$1</span>');
1529
+ }
1530
+ if (category === 'rust') {
1531
+ const rsKeywords = /\b(fn|let|mut|pub|struct|enum|impl|trait|type|use|mod|crate|return|if|else|match|loop|while|for|in|break|continue|async|await|where|unsafe|as|ref|move)\b/g;
1532
+ return escapeHtml(code)
1533
+ .replace(/(\b(?:i8|i16|i32|i64|i128|isize|u8|u16|u32|u64|u128|usize|f32|f64|bool|char|str|String|Vec|Option|Result|Some|None|Ok|Err|true|false)\b)/g, '<span class="tok-type">$1</span>')
1534
+ .replace(rsKeywords, '<span class="tok-keyword">$1</span>')
1535
+ .replace(/(\/\/.*$)/gm, '<span class="tok-comment">$1</span>')
1536
+ .replace(/("(?:\\[\s\S]|[^"\\])*")/g, '<span class="tok-string">$1</span>');
1537
+ }
1350
1538
  const lines = code.split('\n');
1351
1539
  return lines.map(line => {
1352
1540
  const commentIdx = line.indexOf('#');
@@ -1371,10 +1559,12 @@
1371
1559
  if (category === 'json') return highlightJSON(rawCode);
1372
1560
  if (category === 'yaml') return highlightYAML(rawCode);
1373
1561
  if (category === 'javascript') return highlightJS(rawCode);
1562
+ if (category === 'java') return highlightJava(rawCode);
1563
+ if (category === 'cpp') return highlightCpp(rawCode);
1374
1564
  if (category === 'python') return highlightPython(rawCode);
1375
1565
  if (category === 'markup') return highlightHTML(rawCode);
1376
1566
  if (category === 'css') return highlightCSS(rawCode);
1377
- if (category === 'shell' || category === 'config' || category === 'sql') return highlightGenericCode(rawCode, category);
1567
+ if (category === 'shell' || category === 'config' || category === 'sql' || category === 'go' || category === 'rust' || category === 'csharp') return highlightGenericCode(rawCode, category);
1378
1568
  return escapeHtml(rawCode);
1379
1569
  }
1380
1570
 
@@ -1504,7 +1694,7 @@
1504
1694
  lines.forEach((line, idx) => {
1505
1695
  const lineNum = idx + 1;
1506
1696
  const trimmed = line.trim();
1507
- if (trimmed.startsWith('//') || trimmed.startsWith('/*') || trimmed.startsWith('*')) return;
1697
+ if (trimmed.startsWith('//') || trimmed.startsWith('/*') || trimmed.startsWith('*') || trimmed.startsWith('return ')) return;
1508
1698
 
1509
1699
  const classMatch = trimmed.match(/\bclass\s+([a-zA-Z0-9_$]+)/);
1510
1700
  if (classMatch) {
@@ -1534,15 +1724,68 @@
1534
1724
  symbols.push({ id: `line-${lineNum}`, name: 'default export', kind: 'export', line: lineNum, text: `📦 export default` });
1535
1725
  }
1536
1726
  });
1727
+ } else if (category === 'java') {
1728
+ lines.forEach((line, idx) => {
1729
+ const lineNum = idx + 1;
1730
+ const trimmed = line.trim();
1731
+ if (trimmed.startsWith('//') || trimmed.startsWith('/*') || trimmed.startsWith('*') || trimmed.startsWith('throw ') || trimmed.startsWith('return ')) return;
1732
+
1733
+ const pkgMatch = trimmed.match(/^package\s+([a-zA-Z0-9_.]+);/);
1734
+ if (pkgMatch) {
1735
+ symbols.push({ id: `line-${lineNum}`, name: pkgMatch[1], kind: 'package', line: lineNum, text: `📦 package ${pkgMatch[1]}` });
1736
+ return;
1737
+ }
1738
+
1739
+ const classMatch = trimmed.match(/\b(?:public|protected|private|static|final|abstract\s+)*(class|interface|enum|record)\s+([a-zA-Z0-9_]+)/);
1740
+ if (classMatch) {
1741
+ const typeIcon = classMatch[1] === 'interface' ? '🏷️' : classMatch[1] === 'enum' ? '🔢' : '🔷';
1742
+ symbols.push({ id: `line-${lineNum}`, name: classMatch[2], kind: classMatch[1], line: lineNum, text: `${typeIcon} ${classMatch[1]} ${classMatch[2]}` });
1743
+ return;
1744
+ }
1745
+
1746
+ const methodMatch = trimmed.match(/^(?:@\w+\s+)*(?:public|protected|private|static|final|synchronized|abstract|default|native|\s)*(?:<[^>]+>\s+)?([a-zA-Z0-9_<>[],.\s]+(?:\s*\[\s*\])?\s+)?([a-zA-Z0-9_]+)\s*\([^)]*\)\s*(?:throws\s+[a-zA-Z0-9_,\s]+)?\s*\{?$/);
1747
+ if (methodMatch) {
1748
+ const name = methodMatch[2];
1749
+ if (!['if', 'for', 'while', 'switch', 'catch', 'return', 'throw', 'new', 'this', 'super'].includes(name)) {
1750
+ symbols.push({ id: `line-${lineNum}`, name, kind: 'method', line: lineNum, text: `⚡ ${name}()` });
1751
+ }
1752
+ }
1753
+ });
1754
+ } else if (category === 'cpp') {
1755
+ lines.forEach((line, idx) => {
1756
+ const lineNum = idx + 1;
1757
+ const trimmed = line.trim();
1758
+ if (trimmed.startsWith('//') || trimmed.startsWith('/*') || trimmed.startsWith('*') || trimmed.startsWith('#') || trimmed.startsWith('throw ') || trimmed.startsWith('return ')) return;
1759
+
1760
+ const nsMatch = trimmed.match(/^namespace\s+([a-zA-Z0-9_:]+)/);
1761
+ if (nsMatch) {
1762
+ symbols.push({ id: `line-${lineNum}`, name: nsMatch[1], kind: 'namespace', line: lineNum, text: `📦 namespace ${nsMatch[1]}` });
1763
+ return;
1764
+ }
1765
+
1766
+ const classMatch = trimmed.match(/\b(class|struct|union|enum\s+class|enum)\s+([a-zA-Z0-9_]+)/);
1767
+ if (classMatch && !trimmed.endsWith(';')) {
1768
+ symbols.push({ id: `line-${lineNum}`, name: classMatch[2], kind: classMatch[1], line: lineNum, text: `🔷 ${classMatch[1]} ${classMatch[2]}` });
1769
+ return;
1770
+ }
1771
+
1772
+ const funcMatch = trimmed.match(/^(?:(?:inline|virtual|static|constexpr|const|explicit|friend|auto|[a-zA-Z0-9_:<>&*~]+)\s+)+([a-zA-Z0-9_~]+)\s*\([^)]*\)\s*(?:const|noexcept|override|final|\s)*\s*\{?$/);
1773
+ if (funcMatch && !['if', 'for', 'while', 'switch', 'catch', 'return', 'else', 'sizeof', 'decltype'].includes(funcMatch[1])) {
1774
+ symbols.push({ id: `line-${lineNum}`, name: funcMatch[1], kind: 'function', line: lineNum, text: `⚡ fn ${funcMatch[1]}()` });
1775
+ }
1776
+ });
1537
1777
  } else if (category === 'python') {
1538
1778
  lines.forEach((line, idx) => {
1539
1779
  const lineNum = idx + 1;
1540
- const classMatch = line.match(/^class\s+([a-zA-Z0-9_]+)/);
1780
+ const trimmed = line.trim();
1781
+ if (trimmed.startsWith('#')) return;
1782
+
1783
+ const classMatch = trimmed.match(/^classs+([a-zA-Z0-9_]+)/);
1541
1784
  if (classMatch) {
1542
1785
  symbols.push({ id: `line-${lineNum}`, name: classMatch[1], kind: 'class', line: lineNum, text: `🔷 class ${classMatch[1]}` });
1543
1786
  return;
1544
1787
  }
1545
- const funcMatch = line.match(/^(\s*)(?:async\s+)?def\s+([a-zA-Z0-9_]+)/);
1788
+ const funcMatch = line.match(/^(\s*)(?:async\s+)?defs+([a-zA-Z0-9_]+)/);
1546
1789
  if (funcMatch) {
1547
1790
  const prefix = funcMatch[1].length > 0 ? ' ' : '';
1548
1791
  symbols.push({ id: `line-${lineNum}`, name: funcMatch[2], kind: 'function', line: lineNum, text: `${prefix}⚡ def ${funcMatch[2]}()` });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@rooode/dsh-plugin-preview",
3
- "version": "0.1.4",
4
- "description": "DeepSeek Harness Markdown 文档与工作空间文件浏览器右侧预览插件 (支持工作区文件树、JSON 交互结构树/格式化、YAML/JS/TS/Python 多语言语法高亮、代码符号大纲、自动换行与多标签 FileTabs)",
3
+ "version": "0.1.6",
4
+ "description": "DeepSeek Harness Markdown 文档与工作空间文件浏览器右侧预览插件 (支持工作区文件树、JSON 交互结构树/格式化、Java/C++/Python/JS/TS/YAML/Go/Rust 多语言语法高亮与符号大纲、自动换行与多标签 FileTabs)",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "types": "lib/index.d.ts",