diff --git a/.claude/ARCHITECTURE.md b/.claude/ARCHITECTURE.md deleted file mode 100644 index 1c045a6..0000000 --- a/.claude/ARCHITECTURE.md +++ /dev/null @@ -1,223 +0,0 @@ -# texpand 架构设计文档 - -## 项目定位 - -`texpand` 是一个 C/C++ 模板展开工具,面向 Competitive Programming 场景。核心功能:将本地 `#include` 依赖递归展开为单文件,并提供语义安全的代码压缩,方便提交到 OJ 平台。 - -**核心挑战**:在保证对所有极端 C/C++ 语法形式绝对鲁棒的前提下,实现本地 CLI 与 VSCode 虚拟文件系统的跨端复用。 - -## 技术栈 - -| 层面 | 技术 | -|------|------| -| 核心语言 | Rust (edition 2024) | -| 语法解析 | tree-sitter + tree-sitter-cpp | -| 序列化 | serde + toml | -| CLI 框架 | clap (derive) | -| WASM 运行时 | @vscode/wasm-wasi (WASI process 模式) | -| VSCode 扩展 | TypeScript + yo code | - -## Monorepo 结构 - -``` -texpand/ -├── Cargo.toml # workspace 定义 -├── texpand-core/ # 核心逻辑库(I/O 无关) -│ ├── Cargo.toml -│ └── src/ -│ ├── lib.rs # 模块导出 -│ ├── resolver.rs # FileResolver trait -│ ├── parser.rs # tree-sitter 封装 + include 提取 -│ ├── expander.rs # DFS 递归展开 + 预处理上下文跟踪 -│ └── compressor.rs # 语义安全代码压缩 -├── texpand-cli/ # CLI 前端 -│ ├── Cargo.toml -│ └── src/ -│ ├── main.rs -│ └── config.rs # .texpand.toml 解析 -└── texpand-vscode/ # VSCode 扩展前端 - ├── Cargo.toml - ├── src/ - │ └── main.rs # WASI 进程模式入口 - └── extension/ # TypeScript VSCode 扩展 - ├── package.json - ├── tsconfig.json - └── src/ - ├── extension.ts - └── wasm.ts # WASM 加载封装 -``` - -## 核心架构设计 - -### 1. I/O 抽象层(FileResolver) - -核心库通过 trait 与 I/O 解耦,实现 CLI / VSCode 双端复用: - -```rust -pub trait FileResolver { - fn resolve(&self, includer_path: &Path, include_path: &str) -> Result; - fn read_content(&self, resolved_path: &Path) -> Result; -} -``` - -- **CLI 端**:`FsResolver` 在配置的 `include_paths` 中按顺序搜索文件,使用 `std::fs` 读取。 -- **VSCode 端**(WASM):`WasiFsResolver` 在 WASI 沙箱内直接使用 `std::fs` 读写文件。VSCode 工作区目录被映射到 WASI 文件系统,无需 JS 桥接。 - -**为什么用同步方案?** WASI 进程模式下 `std::fs` 操作经过 WASI 层转发到宿主 VSCode,调用本身是阻塞的,但 `@vscode/wasm-wasi` 在扩展主线程中异步等待进程退出,不阻塞 UI。 - -### 2. 依赖解析与展开流程 - -采用 **DFS 递归展开** + **预处理上下文跟踪**: - -1. **解析**:tree-sitter 解析入口文件,生成 AST。 -2. **DFS 游标遍历**:使用 tree-sitter `TreeCursor` 深度优先遍历 AST: - - 遇到 `preproc_include` 节点: - - **Local include** → 调用 `FileResolver::resolve` 解析路径、`read_content` 读取内容,递归展开后拼入输出,跳过 children。 - - **System include** → 保留原始文本,标记为已展开(避免重复输出)。 - - 遇到 `preproc_call` 且内容为 `#pragma once` → 跳过该节点(不输出)。 - - 遇到 compound directive(`preproc_ifdef`/`preproc_ifndef`/`preproc_if`)→ 将条件 subject 压入 `PreprocContext` 栈,退出时弹出。 - - 遇到 `#else`/`#elif`/`#elifdef` → 推入 `PreprocContext`。 - - 叶子节点(非 comment)→ 直接输出原始文本。 -3. **上下文感知去重**:同一文件在**不同预处理上下文**中(例如 `#ifdef X` 和 `#else` 分支)会分别展开。去重键为 `(resolved_path, PreprocContext)` 二元组。 -4. **循环检测**:通过 `expanding: HashSet` 跟踪当前调用栈,重复进入则报 circular dependency。 -5. **系统 include**:保留 `#include <...>` 原行,不递归展开。 - -**为什么 DFS 而非 BFS + 拓扑排序?** 展开需要在遍历 AST 的同时按源码顺序增量输出。DFS + tree-sitter `TreeCursor` 天然支持 walk-and-emit 模式,且能无缝跟踪预处理指令的嵌套结构。原先基于 petgraph 的图展开方案因需要额外的拓扑排序和分离的展开阶段,已被内联到 DFS walk 中。 - -### 3. 代码压缩(Compressor) - -基于 `CompressorState` 状态机的 AST 单遍压缩。核心规则: - -``` -遍历 AST(非叶子节点也进入,管理 preproc 状态): - -叶子节点处理: - ├── 注释 (kind == "comment") → 丢弃 - ├── user_defined_literal(如 123_km)→ 不插空格直接拼接 - ├── #define 的 name field → 追加尾部空格(防止 FOO"bar" 合并) - ├── 标识符相邻 (prev_last 和当前首字符均为 [a-zA-Z0-9_]) → 强制插入空格 - └── 其他情况 → 直接拼接 - -预处理指令保护: - ├── 进入任何 preproc_* 节点 → 确保当前行以 \n 结尾 - ├── compound 指令 (#ifdef / #if) → 跟踪 body 起始位置,在 body 前插入 \n - └── 离开 preproc_* 节点 → 追加 \n -``` - -**`#define` 名与替换文本之间**:插入空格以保证 `#define FOO"bar"` 不变成非法语法。但函数式宏(`#define FOO(x)`)检测到 `name` 后紧跟 `(` 时不插空格。 - -**`compress_stripped` 变体**:在单遍压缩的同时跳过 `preproc_include` 和 `#pragma once` 子树,避免二次解析。 - -**安全原则**:压缩优先保证语义不变,其次追求体积最小。 - -**为什么不用正则?** C/C++ 语法极其复杂,正则无法正确处理所有边界情况(宏嵌套、条件编译、字符串字面量中的注释等)。tree-sitter AST 是唯一可靠的方式。 - -### 4. 边界 Case 处理 - -| 场景 | 处理方式 | -|------|----------| -| 循环 `#include` | `expanding: HashSet` 运行时检测 → 报 circular dependency | -| `#include ` | 保留原行,不入展开队列 | -| `#include "local"` | strip 原行,递归展开内容 | -| `#pragma once` | tree-sitter 匹配到 `preproc_call` 内容为 `#pragma once` → 跳过该节点 | -| 条件编译中的 include (`#ifdef` + `#include`) | 跟踪 `PreprocContext` 栈,同文件在每个条件分支中独立展开 | -| 同文件多次 include(同一上下文) | 去重键 `(resolved_path, PreprocContext)` → 仅展开一次 | -| 压缩后 `inta` 变成 `int a`? | 不需要 — 标识符隔离规则保证 | -| 压缩后 `123_km` 变成 `123 _km`? | 不需要 — `literal_suffix` 节点标记为 `skip_space_before` | -| `#define FOO"bar"` | 压缩器在 `name` field 后追加空格 → `#define FOO "bar"` | -| `#define FOO(x) (x)` (函数式宏) | 检测到 `name` 后紧跟 `(` → 不插空格 | -| `#include"foo.h"` 无空格 | C 预处理器接受此语法,压缩器不会额外插入空格 | -| 嵌套 preproc (`#if` 内 `#include`) | 每个 preproc 节点退出时追加 `\n` | -| 压缩后 `#define A\n#define B` 合并? | 不会 — 每个 preproc 退出时强制 `\n` | - -## 安全约束 - -- `texpand-core` **绝对禁止**直接调用 `std::fs` / `std::io`。所有文件读取通过 `FileResolver` trait。 -- **禁止**使用正则表达式解析或匹配 C/C++ 语法(包括 include 路径提取、注释检测等)。所有语法分析必须通过 tree-sitter AST。 - -## 数据流全景 - -``` - ┌──────────────┐ - │ 入口 .cpp │ - └──────┬───────┘ - │ source text - ▼ - ┌───────────────────────────┐ - │ texpand-core │ - │ │ - │ parse_source() │ - │ │ │ - │ ▼ │ - │ tree-sitter Tree │ - │ │ │ - │ ▼ │ - │ DFS TreeCursor walk ◄──┤ ← FileResolver (trait) - │ │ │ - │ ┌────┴───────────┐ │ - │ │ preproc_include│ │ - │ └─┬───┘ │ │ - │ │ │ │ - │ ▼ │ │ - │ Local? │ │ - │ ├── Yes ──→ resolve() │ - │ │ │ │ - │ │ ▼ │ - │ │ read_content() │ - │ │ │ │ - │ │ expand_recursive│ - │ │ (DFS, stack) │ - │ └── No ──→ 保留原行 │ - │ │ - │ ┌──────────────────┐ │ - │ │ PreprocContext │ │ - │ │ 栈跟踪条件分支 │ │ - │ │ (#ifdef/#if/..) │ │ - │ └──────┬───────────┘ │ - │ │ │ - │ 去重键: (path, ctx) │ - │ │ - │ 循环检测: expanding Set │ - │ │ - │ 输出: ──→ 原始文本 emit │ - │ 或 ──→ CompressorState │ - └──────────┬────────────────┘ - │ 展开后单文件 - ▼ - ┌────────────────┐ - │ CLI: stdout/文件 │ - │ VS: 剪贴板/新文件│ - └────────────────┘ -``` - -## VSCode 扩展架构 - -通过 `@vscode/wasm-wasi` 以 **WASI 进程模式**运行 Rust 编译的 WebAssembly 模块: - -``` -VSCode 扩展进程 WASM 沙箱 (texpand-vscode) - (wasm32-wasip1) -┌─────────────────────────┐ ┌──────────────────────────────┐ -│ extension.ts │ │ main.rs (WASI process) │ -│ │ │ │ -│ 1. 监听 C/C++ 文件激活 │ │ TEXPAND_ENTRY_PATH ─────→ main() │ -│ 2. 构造 WASM 进程 │ │ TEXPAND_COMPRESS │ -│ (@vscode/wasm-wasi) │ │ TEXPAND_INCLUDE_PATHS │ -│ 3. 环境变量传参 │ env │ │ │ -│ 4. 读取 stdout │ ←── │ WasiFsResolver │ -│ │ │ ├── resolve() │ -│ 交互: │ │ └── read_content() │ -│ ├── 编辑器标题栏按钮 │ │ │ │ -│ ├── 右键上下文菜单 │ │ std::fs (via WASI) │ -│ └── 状态栏 QuickPick │ └──────────┬───────────────────┘ -│ │ │ -│ 输出: clipboard / 新文件 │ JSON stdout {success, data?, error?} -└─────────────────────────┘ └──────────────────────────────┘ -``` - -**关键设计**: -- 扩展依赖 `ms-vscode.wasm-wasi-core` 扩展提供 WASI 运行时。 -- 参数通过环境变量传递(`TEXPAND_ENTRY_PATH`, `TEXPAND_COMPRESS`, `TEXPAND_INCLUDE_PATHS`)。 -- `WasiFsResolver` 实现 `FileResolver` trait,直接使用 `std::fs` 读写(WASI 沙箱映射了工作区目录)。 -- 结果以 JSON 格式写入 stdout。 -- 编译目标:`wasm32-wasip1`,使用 `wasm-opt` 后处理优化。 diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index e29b0dd..9189cb2 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -1,56 +1,74 @@ -## 项目定位与目标 +# CLAUDE.md -`texpand` 是一个 C/C++ 模板展开工具。其核心挑战在于:**在保证对所有极端 C/C++ 语法形式绝对鲁棒的前提下,实现本地代码与 VSCode 虚拟文件系统的跨端复用。** +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. -## 核心技术栈 +## Build & Test Commands -- **开发语言**:Rust (Workspaces / Monorepo 组织形式) -- **语法解析**:`tree-sitter` & `tree-sitter-cpp`(绝对禁止使用正则表达式匹配 C++ 语法) -- **图算法**:`petgraph`(用于依赖路径解析和环路检测) -- **序列化**:`serde` + `toml` -- **WASM 绑定**:`wasm-bindgen`(用于编译 VSCode 扩展底层的 WebAssembly 模块) +```bash +# Build entire workspace +cargo build --workspace -## 项目结构与 Monorepo 划分 +# Run all tests +cargo test --workspace -项目包含三个子 crate,职责必须严格隔离: +# Run a single test +cargo test test_name -1. **`texpand-core`**:核心逻辑。**约束:** 必须保持 I/O 无关,绝对禁止直接调用 `std::fs` 或 `std::io`。所有文件读取必须通过 `FileResolver` Trait 抽象。 -2. **`texpand-cli`**:CLI 前端。**职责:** 解析命令行参数 (clap),实现 `FileResolver` 以读取本地磁盘文件,调用 `texpand-core` 并输出结果。 -3. **`texpand-vscode`**:VSCode 扩展前端。**职责:** 通过 TypeScript 调用 VSCode API 读取文件,将数据传递给通过 `wasm-bindgen` 编译的 Rust WASM 模块。 +# Run tests for a specific crate +cargo test -p texpand-core -## 核心架构设计 +# Run integration tests only +cargo test --test test_basic_expansion -### 1. I/O 抽象 +# Format and lint +cargo fmt --all +cargo clippy --workspace -- -D warnings -为了实现跨端,核心库定义了如下数据获取接口: +# VSCode extension typecheck (from texpand-vscode/extension/) +npm run typecheck -```rust -// texpand-core/src/resolver.rs -pub trait FileResolver { - // 传入 includer 路径和 #include 路径,返回解析后的绝对路径 - fn resolve(&self, includer_path: &Path, include_path: &str) -> Result; - // 根据解析后的路径读取文件内容 - fn read_content(&self, resolved_path: &Path) -> Result; -} +# Build WASM binary for VSCode extension +cargo build -p texpand-vscode --target wasm32-wasip1 --release + +# Full VSCode extension build (WASM + esbuild + package into .vsix) +npm run vscode:prepublish # from texpand-vscode/extension/ +npm run package # create .vsix via vsce ``` -- CLI 端:使用 `std::fs` 根据配置的 `include_paths` 搜索并读取。 -- WASM 端:TypeScript 端封装 `vscode.workspace.fs.readFile`,Rust 端通过 `extern "C"` 导入该 JS 异步/同步函数来实现 Trait。 +## Project Architecture + +Monorepo with 3 crates — a core Rust library with two frontends (CLI + VSCode extension via WASM). + +### `texpand-core` (I/O-free core library) +The central processing engine. **Must never call `std::fs`/`std::io` directly** — all file reading goes through the `FileResolver` trait. + +- **`resolver.rs`** — `FileResolver` trait: `resolve()` + `read_content()`. CLI implements via `std::fs`, VSCode via WASI filesystem. +- **`parser.rs`** — tree-sitter C/C++ parser wrapper. `parse_source()` → AST tree, `extract_all_includes()` → Local/System include classification. +- **`expander.rs`** — BFS-based recursive expansion. Tracks `PreprocContext` (conditional directive stack) for correct dedup inside `#ifdef`/`#if` branches. Walks the AST, processes `preproc_include`, `#pragma once`, compound conditionals. +- **`compressor.rs`** — Token-level compressor via AST leaf walk. Drops comments, inserts space between adjacent identifier chars, forces newlines around preproc directives. `CompressorState` is a reusable state machine. -### 2. 依赖展开路径 +### `texpand-cli` (CLI frontend) +- `clap`-based argument parsing. +- `FsResolver` implements `FileResolver` with `std::fs`. +- Config from `~/.config/texpand.toml` (`include_paths`, `default_compress`). +- Clipboard support via `arboard` (forks on Linux for persistence). -1. 利用 Tree-sitter 解析文件,过滤提取出 `preproc_include` 节点。 -2. 通过 `FileResolver` 获取文件内容。 -3. 按顺序扫描代码,递归展开。 +### `texpand-vscode` (VSCode extension frontend) +- **Rust WASM layer** (`src/main.rs`): WASI process entry point. Reads env vars (`TEXPAND_ENTRY_PATH`, `TEXPAND_COMPRESS`, `TEXPAND_INCLUDE_PATHS`), calls `texpand-core` expand, prints JSON result to stdout. +- **TypeScript extension** (`extension/`): Activates on C/C++ files. 3 commands: expandDefault, expandAndCopy, expandToNewFile. Loads WASM via `@vscode/wasm-wasi`, mounts workspace files, reads result. -### 3. 代码压缩安全原则 +### Tests +- **Unit tests**: `#[cfg(test)] mod tests` inside each source file in `texpand-core`. +- **Integration tests**: `texpand-core/tests/` — use `FixtureResolver` (in-memory file map implementing `FileResolver`). +- **Fixtures**: `fixtures/` directory with real C/C++ files for CLI end-to-end testing. -代码压缩旨在减小体积,但**语义安全是最高优先级**。压缩逻辑基于 Tree-sitter 的 AST 叶子节点: +## Key Constraints -* **注释丢弃**:直接丢弃 `kind == "comment"` 的节点。 -* **标识符隔离**:维护状态机。如果相邻的两个 Token,前一个的尾字符是 `[a-zA-Z0-9_]` 且后一个的首字符也是 `[a-zA-Z0-9_]`,则它们之间**必须强制插入一个空格**。 -* **符号紧凑**:除上述情况外,纯符号(如 `{`, `+`, `;`)之间直接拼接,不加空格。 +- `texpand-core` must stay I/O-free — no `std::fs`, no `std::io`. All data comes through `FileResolver`. +- Clippy must pass with `-D warnings`. +- Rust edition 2024; let-chains style preferred. +- The `graph.rs` module described in ARCHITECTURE.md was inlined into `expander.rs` — the dependency graph and cycle detection live there now. -### 4. 关键边界 Case 处理 (预处理指令截断) +## Localization -**注意:** C/C++ 预处理指令(`#define`, `#include` 等)对换行符敏感。基础压缩会抹除所有换行,这会导致预处理指令吞噬后续代码。在遍历 AST 时,如果游标进入任何 `preproc_*` 节点,必须在其遍历结束(离开该节点作用域)时,向输出缓冲区**强制追加一个换行符 `\n`**。 +VSCode extension uses `@vscode/l10n`. Localized strings in `l10n/bundle.l10n.{locale}.json`. Package manifest strings in `package.nls.{locale}.json`. To export strings for translation: `npm run l10n:export` from `texpand-vscode/extension/`. diff --git a/.claude/PROGRESS.md b/.claude/PROGRESS.md deleted file mode 100644 index a797073..0000000 --- a/.claude/PROGRESS.md +++ /dev/null @@ -1,85 +0,0 @@ -# texpand 项目进度 - -## 总体状态 - -| Phase | 描述 | 状态 | -|-------|------|------| -| 1 | Workspace 骨架搭建 | ✅ 完成 | -| 2 | texpand-core 核心逻辑 | ✅ 完成 | -| 3 | texpand-cli CLI 前端 | ✅ 完成 | -| 4 | texpand-vscode VSCode 扩展 | 🏗️ 进行中 | -| 5 | 边缘 Case 加固与文档 | ⏳ 待开始 | - -## Phase 1: Workspace 骨架搭建 ✅ - -- [x] 根 `Cargo.toml` → workspace 定义(3 成员) -- [x] `texpand-core/` lib crate 创建 -- [x] `texpand-cli/` bin crate 创建 -- [x] `texpand-vscode/` cdylib crate 创建 -- [x] 依赖添加:tree-sitter, tree-sitter-cpp, clap, serde, toml, anyhow, @vscode/wasm-wasi -- [x] 旧 `src/main.rs` 移除 -- [x] `cargo check --workspace` ✅ - -## Phase 2: texpand-core 核心逻辑 ✅ - -### resolver.rs -- [x] `FileResolver` trait 定义 - -### parser.rs -- [x] `parse_source()` — tree-sitter 封装 -- [x] `extract_all_includes()` — 提取 Local / System include -- [x] `extract_include_paths()` — 仅 Local include(BFS 用) -- [x] `is_quoted_include()` — 判断是否为本地 include - -### expander.rs -- [x] `expand()` — DFS 递归展开(tree-sitter 游标遍历) -- [x] Local include → strip + 递归解析 -- [x] System include → 保留原行 + 缓存 -- [x] 预处理上下文跟踪(`PreprocContext`)— 同文件不同条件分支独立展开 -- [x] `#pragma once` 剥离 -- [x] 循环依赖检测(`expanding: HashSet`) -- [x] 可选 compression 分支 - -### compressor.rs -- [x] 注释丢弃 -- [x] 标识符隔离(`[a-zA-Z0-9_]` 相邻时插空格) -- [x] 符号紧凑 -- [x] preproc 节点换行保护 -- [x] let-chains 风格(Rust edition 2024) - -### 测试覆盖 -- [x] 25 个单元测试全部通过 -- [x] `cargo clippy --all-targets -- -D warnings` 零警告 -- [x] `cargo fmt --all` 通过 - -## Phase 3: texpand-cli ✅ - -- [x] CLI args(clap):`INPUT`, `-c`, `--no-compress`, `-i`, `-o`, `-C`, `--config` -- [x] `config.rs`:`include_paths`, `default_compress` TOML 解析 -- [x] `FsResolver`:`FileResolver` 的 `std::fs` 实现(支持 includer 相对路径解析) -- [x] pipeline 组装 -- [x] 修复 expander 核心 bug:图节点键名需使用 canonicalized 路径而非原始 include 路径 -- [x] 改进 `FileResolver` trait:新增 `includer_path` 参数,支持嵌套目录相对 `#include` 解析 -- [x] 添加测试用 fixtures(`fixtures/`):basic, deep, diamond, system-include, pragma-once, compress, multiple, nested, circular - -## Phase 4: texpand-vscode 🏗️ - -- [x] VSCode 扩展脚手架(yo code) -- [x] WASM 编译(wasm32-wasip1, WASI SDK) -- [x] `WasiFsResolver` — 利用 WASI 文件系统的 `std::fs` 实现 -- [x] 3 个注册命令(expandDefault, expandAndCopy, expandToNewFile) -- [x] 编辑器标题栏按钮(`$(copy)` 图标,c/c++ 专用) -- [x] 右键上下文菜单(Texpand 子菜单,含两个选项) -- [x] 底部状态栏(配置 QuickPick:压缩、输出模式、搜索路径) -- [x] 端到端集成调试(需要实际 VSCode 实例加载扩展) -- [x] 扩展打包与发布(vsce package) - -## Phase 5: 边缘 Case 加固 ⏳ - -- [ ] 极端 C/C++ 语法测试样例 -- [ ] 补充单元测试 -- [ ] 各 crate API 文档 - -## 已知技术债务 - -_(所有已知债务已解决)_ diff --git a/.claude/project-tracker/.meta b/.claude/project-tracker/.meta new file mode 100644 index 0000000..09d795b --- /dev/null +++ b/.claude/project-tracker/.meta @@ -0,0 +1,37 @@ +files: + INDEX.md: + baseline: 65b49e172b12d9f2eda018c1f2e146eaacaf9c10 + updated: 2026-05-12T00:42:00Z + stack.md: + baseline: 65b49e172b12d9f2eda018c1f2e146eaacaf9c10 + updated: 2026-05-12T00:42:00Z + toolchain.md: + baseline: 65b49e172b12d9f2eda018c1f2e146eaacaf9c10 + updated: 2026-05-12T00:42:00Z + architecture.md: + baseline: 65b49e172b12d9f2eda018c1f2e146eaacaf9c10 + updated: 2026-05-12T00:42:00Z + progress.md: + baseline: 65b49e172b12d9f2eda018c1f2e146eaacaf9c10 + updated: 2026-05-12T00:42:00Z + implementation.md: + baseline: 65b49e172b12d9f2eda018c1f2e146eaacaf9c10 + updated: 2026-05-12T00:42:00Z + data-model.md: + baseline: 65b49e172b12d9f2eda018c1f2e146eaacaf9c10 + updated: 2026-05-12T00:42:00Z + api.md: + baseline: 65b49e172b12d9f2eda018c1f2e146eaacaf9c10 + updated: 2026-05-12T00:42:00Z + deployment.md: + baseline: 65b49e172b12d9f2eda018c1f2e146eaacaf9c10 + updated: 2026-05-12T00:42:00Z + modules/core.md: + baseline: 65b49e172b12d9f2eda018c1f2e146eaacaf9c10 + updated: 2026-05-12T00:42:00Z + modules/cli.md: + baseline: 65b49e172b12d9f2eda018c1f2e146eaacaf9c10 + updated: 2026-05-12T00:42:00Z + modules/vscode-extension.md: + baseline: 65b49e172b12d9f2eda018c1f2e146eaacaf9c10 + updated: 2026-05-12T00:42:00Z diff --git a/.claude/project-tracker/INDEX.md b/.claude/project-tracker/INDEX.md new file mode 100644 index 0000000..6ce76d4 --- /dev/null +++ b/.claude/project-tracker/INDEX.md @@ -0,0 +1,49 @@ +# Template-Expand + +C/C++ `#include` template expansion tool for Competitive Programming. Expands local headers into a single file, with optional semantic-safe token-level compression. Ships as both a CLI tool (`texpand`) and a VSCode extension. + +## Table of Contents + +- [Index](INDEX.md) — this file +- [Stack](stack.md) — technology choices & rationale +- [Toolchain](toolchain.md) — build, lint, test, CI/CD +- [Architecture](architecture.md) — module layout & data flow +- [Progress](progress.md) — current status & roadmap +- [Implementation](implementation.md) — entry points & key logic +- [Data Model](data-model.md) — core types & state +- [API](api.md) — CLI surface & extension commands +- [Deployment](deployment.md) — building & packaging +- [Modules](modules/) — per-crate deep dives + - [Core](modules/core.md) — `texpand-core` library + - [CLI](modules/cli.md) — `texpand-cli` frontend + - [VSCode Extension](modules/vscode-extension.md) — `texpand-vscode` frontend + +## Tech Stack Summary + +- **Language**: Rust 2024 edition (stable toolchain) +- **Parser**: tree-sitter C/C++ grammar for AST-based include analysis +- **CLI**: clap argument parsing, `~/.config/texpand.toml` config +- **VSCode**: WASM-WASI process mode via `@vscode/wasm-wasi`, TypeScript extension host +- **Serialization**: serde/serde_json for structured output + +## Quick-Reference Commands + +```bash +# Build +cargo build --workspace + +# Test +cargo test --workspace + +# Lint +cargo clippy --workspace -- -D warnings + +# Format +cargo fmt --all + +# Run CLI +cargo run -p texpand-cli -- main.cpp -c + +# Build VSCode WASM +cargo build -p texpand-vscode --target wasm32-wasip1 --release +``` diff --git a/.claude/project-tracker/api.md b/.claude/project-tracker/api.md new file mode 100644 index 0000000..f8174f0 --- /dev/null +++ b/.claude/project-tracker/api.md @@ -0,0 +1,72 @@ +# API Surface + +## CLI (`texpand`) + +### Command +``` +texpand [OPTIONS] + INPUT: Path to C/C++ source file (use "-" for stdin) +``` + +### Options + +| Flag | Description | +|------|-------------| +| `-c, --compress` | Enable token-level compression (overrides config) | +| `--no-compress` | Disable compression (overrides config) | +| `-i, --include ` | Add include search path (repeatable, overrides config) | +| `-o, --output ` | Write to file instead of stdout | +| `-C, --clipboard` | Copy to clipboard (mutually exclusive with `-o`) | +| `--config ` | Config file path (default: `~/.config/texpand.toml`) | + +### Exit Codes +- 0: Success +- Non-zero: Error (anyhow prints diagnostic to stderr) + +## VSCode Extension + +### Commands +| Command ID | Title | Behavior | +|-----------|-------|----------| +| `texpand.expandDefault` | Texpand: Expand Current File (Default) | Uses configured `outputMode` | +| `texpand.expandAndCopy` | Texpand: Expand and Copy | Forces clipboard output | +| `texpand.expandToNewFile` | Texpand: Expand to New File | Creates `.expanded.cpp` | + +### Activation +Events: `onLanguage:c`, `onLanguage:cpp` + +### WASM Process Protocol +The TypeScript extension: +1. Spawns a WASI process from the WASM binary +2. Sets env vars: `TEXPAND_ENTRY_PATH`, `TEXPAND_COMPRESS`, `TEXPAND_INCLUDE_PATHS` +3. Mounts workspace filesystem paths +4. Reads JSON from WASM stdout: `{ success: bool, data?: string, error?: string }` +5. Displays result (clipboard or new file) + +## Core Library (`texpand-core`) + +### Public API +```rust +// expander.rs +pub fn expand( + entry_path: &Path, + entry_source: &str, + resolver: &dyn FileResolver, + opts: &ExpandOptions, +) -> Result + +pub struct ExpandOptions { + pub compress: bool, +} + +pub trait FileResolver { + fn resolve(&self, includer_path: &Path, include_path: &str) -> Result; + fn read_content(&self, resolved_path: &Path) -> Result; +} + +// Standalone compression +pub fn compress(tree: &Tree, source: &str) -> String; +pub fn compress_stripped(tree: &Tree, source: &str) -> String; +``` + +No REST API, no HTTP endpoints — this is a local-only tool. diff --git a/.claude/project-tracker/architecture.md b/.claude/project-tracker/architecture.md new file mode 100644 index 0000000..7d6725d --- /dev/null +++ b/.claude/project-tracker/architecture.md @@ -0,0 +1,72 @@ +# Architecture + +## Overview + +Monorepo with a shared core library and two frontends. The core is I/O-free — all file reading goes through the `FileResolver` trait, allowing each frontend to provide its own storage backend. + +``` +┌──────────────────────────────────────────────────────┐ +│ texpand-core │ +│ ┌──────────┐ ┌──────────┐ ┌──────────────────┐ │ +│ │ parser │ │ expander │ │ compressor │ │ +│ │ (tree- │ │ (BFS │ │ (token-level AST │ │ +│ │ sitter) │ │ expand) │ │ leaf walk) │ │ +│ └────┬─────┘ └────┬─────┘ └────────┬─────────┘ │ +│ │ │ │ │ +│ ┌────┴──────────────┴──────────────────┴─────────┐ │ +│ │ resolver.rs (FileResolver trait) │ │ +│ │ resolve() + read_content() — no std::fs calls │ │ +│ └────────────────────┬────────────────────────────┘ │ +└───────────────────────┼──────────────────────────────┘ + │ + ┌─────────────┼─────────────┐ + │ │ │ +┌─────────┴──────┐ ┌───┴────────┐ ┌──┴────────────┐ +│ texpand-cli │ │ texpand- │ │ texpand- │ +│ FsResolver │ │ vscode │ │ vscode │ +│ (std::fs) │ │ (WASI fs) │ │ (TypeScript) │ +│ │ │ │ │ │ +│ clap CLI │ │ env-var │ │ VSCode │ +│ config/toml │ │ config │ │ commands │ +│ arboard clip │ │ JSON stdout│ │ WASM launcher │ +└────────────────┘ └────────────┘ └───────────────┘ +``` + +## Module Breakdown (texpand-core) + +| Module | File | Responsibility | +|--------|------|---------------| +| `resolver` | `resolver.rs` | `FileResolver` trait — abstract file I/O | +| `parser` | `parser.rs` | tree-sitter C/C++ parse + include extraction | +| `expander` | `expander.rs` | BFS recursive expansion, cycle detection, preproc context tracking | +| `compressor` | `compressor.rs` | Token-level AST leaf walk compression | + +## Key Data Flow + +``` +Source file ──→ parser.rs ──→ AST tree + │ + ▼ + extract_includes() + │ + ▼ + expander.rs ──→ FileResolver.resolve() + │ │ + ▼ ▼ + Recursive expand ──→ read_content() + │ + ▼ + Compressor (optional) ──→ Final string output +``` + +## Security Boundaries + +- **I/O boundary**: `FileResolver` trait is the sole I/O interface — no `std::fs` or `std::io` in `texpand-core` +- **WASM sandbox**: VSCode extension runs in WASI process with virtual filesystem — no access to host filesystem beyond workspace +- **Fork isolation**: Linux clipboard uses `fork()` to persist clipboard content — child process exits independently + +## Design Patterns + +- **Trait-based abstraction**: `FileResolver` enables two completely different I/O backends (std::fs, WASI) from the same core +- **BFS with context-sensitive dedup**: `PreprocContext` stack tracks conditional directive state so `#include` inside `#ifdef`/`#else` branches is correctly handled +- **Reusable state machine**: `CompressorState` can be reused across multiple tree walks, tracking compound directive nesting and body newline insertion diff --git a/.claude/project-tracker/data-model.md b/.claude/project-tracker/data-model.md new file mode 100644 index 0000000..6037fa4 --- /dev/null +++ b/.claude/project-tracker/data-model.md @@ -0,0 +1,88 @@ +# Data Model + +## Core Types + +### `FileResolver` trait (resolver.rs) +``` +FileResolver +├── resolve(includer_path, include_path) to Result +└── read_content(resolved_path) to Result +``` + +The central I/O boundary. Two implementations: +- `FsResolver` (CLI): `std::fs::canonicalize` + `read_to_string` +- `WasiFsResolver` (VSCode): WASI filesystem paths + +### Include types (parser.rs) +``` +Include<'a> +├── Local(&'a str) // #include "path" +└── System(&'a str) // #include +``` + +### Preproc directive stack (expander.rs) +``` +PreprocDirective +├── If(Subject) +├── Ifdef(Subject) +├── Ifndef(Subject) +├── Elif(Subject) +├── Elifdef(Subject) +└── Else + +PreprocContext(Vec) +``` + +The `Subject` is a `Vec` — the token sequence extracted from the conditionals argument for structural equivalence comparison. + +### ExpandState (expander.rs) +``` +ExpandState +├── completed: HashSet<(PathBuf, PreprocContext)> +│ Files + context pairs already expanded (dedup key) +├── expanding: HashSet +│ Cycle detection stack +└── tree_cache: HashMap +│ Parsed AST cache +``` + +### CompressorState (compressor.rs) +``` +CompressorState +├── output: String // Accumulated compressed output +├── prev_last: Option // Last emitted char (identifier spacing) +├── compound_depth: usize // #ifdef / #if nesting depth +└── body_nl_counter: Option + Sibling counter for body newline insertion +``` + +## Config Model + +### CLI config (texpand-cli/config.rs) +```toml +# ~/.config/texpand.toml +include_paths = ["./templates", "~/algo/cpp_lib"] +default_compress = false +``` + +### VSCode settings +| Key | Type | Default | +|-----|------|---------| +| `texpand.includePaths` | `string[]` | `["./"]` | +| `texpand.defaultCompression` | `boolean` | `false` | +| `texpand.outputMode` | `"clipboard"` or `"newFile"` | `"clipboard"` | +| `texpand.saveBeforeExpansion` | `boolean` | `true` | + +## VSCode Result Envelope +``` +ExpandResult (JSON to stdout) +├── success: bool +├── data: Option // Expanded output +└── error: Option // Error message +``` + +## Key Invariants + +- `texpand-core` must never call `std::fs` or `std::io` — all file data enters through `FileResolver` +- `PreprocContext` is purely structural — no actual macro evaluation; same token sequence = same context +- Dedup key `(PathBuf, PreprocContext)` means the same file CAN appear multiple times if included under different `#ifdef` branches diff --git a/.claude/project-tracker/deployment.md b/.claude/project-tracker/deployment.md new file mode 100644 index 0000000..81403fe --- /dev/null +++ b/.claude/project-tracker/deployment.md @@ -0,0 +1,59 @@ +# Deployment + +## Build Artifacts + +### CLI Binary +- **Format**: Single platform-native executable +- **Targets**: x86_64 Linux, ARM64 Linux, x86_64 macOS, ARM64 macOS, x86_64 Windows +- **Build**: `cargo build -p texpand-cli --release --target ` +- **Size**: ~3-5 MB per binary (stripped, LTO) +- **Install**: `cargo install --path texpand-cli` or download from GitHub Releases + +### VSCode Extension +- **Format**: `.vsix` package +- **Contents**: WASM binary (wasi target) + bundled TypeScript +- **Build**: + 1. `cargo build -p texpand-vscode --target wasm32-wasip1 --release` + 2. `wasm-opt` for optimization + 3. `esbuild` for TypeScript bundling + 4. `vsce package` for .vsix +- **Published**: GitHub Releases (not on Marketplace) + +## Release Process + +Trigger: push tag `v*` to GitHub. + +``` +Release workflow (release.yml): +├── build-cli (matrix: 5 targets) +│ ├── Install Rust + cross-compilers +│ ├── cargo build --release +│ ├── Rename to texpand- +│ └── Upload artifact +├── build-vsix (ubuntu-latest) +│ ├── Install WASI SDK 33 +│ ├── npm ci +│ ├── npm run vscode:prepublish (WASM + esbuild) +│ ├── vsce package +│ └── Upload artifact +└── release (after both complete) + ├── Download all artifacts + └── Create GitHub Release with generated notes +``` + +## Environments + +| Environment | Distribution | Update Mechanism | +|-------------|-------------|-----------------| +| Local dev | `cargo build` | Manual rebuild | +| End-user CLI | GitHub Releases / `cargo install` | Manual | +| VSCode | `.vsix` sideload | Manual install from VSIX | + +## Health Checks & Monitoring + +N/A — local CLI tool with no server component. The VSCode extension writes diagnostic messages to stderr (visible in output channel). No telemetry or crash reporting. + +## Rollback + +- CLI: reinstall previous version via `cargo install --version ` or download old release binary +- VSCode: Install from VSIX with previous version, or uninstall/reinstall diff --git a/.claude/project-tracker/implementation.md b/.claude/project-tracker/implementation.md new file mode 100644 index 0000000..c90e5ce --- /dev/null +++ b/.claude/project-tracker/implementation.md @@ -0,0 +1,79 @@ +# Implementation Details + +## Entry Points + +| Crate | Entry | Mechanism | +|-------|-------|-----------| +| texpand-cli | `main()` in `main.rs` | `clap::Parser` to `expand()` | +| texpand-vscode | `main()` in `src/main.rs` | WASI process, env-var config to `expand()` to JSON stdout | +| VSCode TS | `extension.ts` | VSCode activation events to WASM lifecycle to result display | + +## Request Trace (CLI) + +``` +cli/main.rs:main() + → Cli::parse() # clap argument parsing + → config::TexpandConfig::load() # optional ~/.config/texpand.toml + → FsResolver::new() # std::fs-backed resolver + → expander::expand() # recursive BFS expansion + → parser::parse_source() # first parse + → expand_recursive() # DFS AST walk + → classify_include() # identify Local vs System includes + → FileResolver::resolve() # resolve #include path + → FileResolver::read_content() # read included file + → expand_recursive() # recurse into dependency + → CompressorState (optional) # token-level compression + → output (clipboard/file/stdout) +``` + +## Key Algorithms + +### BFS Expansion with Preproc Context + +In `expander.rs`, `expand_recursive()` walks the AST in DFS order. For each node: + +1. **`preproc_include`**: resolve local includes via `FileResolver`, recurse, inline result. System includes preserved as-is. +2. **`#pragma once`**: stripped entirely. +3. **Compound conditionals** (`#ifdef`, `#ifndef`, `#if`): push `PreprocDirective` onto a context stack. The context tracks which conditional branch we are in. +4. **Dedup key**: `(file_path, PreprocContext)` — the same file can be expanded in different conditional contexts. + +### Cycle Detection + +Uses a `expanding: HashSet` stack. If a file is encountered while already on the stack, the full cycle path is reported in the error. This is set-based, not graph-based — linear in expansion depth. + +### Token-Level Compression + +`compressor.rs` walks AST leaves only: +- Comments are skipped entirely (tree-sitter `"comment"` node kind) +- Space inserted between adjacent identifier characters (`[a-zA-Z0-9_]`) +- Before/after `#` preprocessor directives: force newline +- Compound preproc bodies (`#ifdef ... #endif`): insert newline before body +- `literal_suffix` nodes: no space inserted (preserves `123_km`) +- `#define` name field: trailing space forced (prevents `#define FOO"abc"`) + +## Error Handling + +- `anyhow::Result` throughout both CLI and WASM frontends +- `.with_context()` for descriptive error messages +- Cycle detection produces a human-readable path +- VSCode WASM wraps errors in JSON `{ success: false, error: "..." }` envelope + +## Testing Strategy + +- **Unit tests**: inline in each source file (`#[cfg(test)] mod tests`) +- **Integration tests**: `tests/` directory with `FixtureResolver` providing in-memory file content +- **Fixtures**: `fixtures/` directory with real C/C++ files for CLI end-to-end +- **Key test scenarios**: + - Basic expansion / transitive dependencies / diamond deps + - Circular dependency detection + - Compression (comments, identifiers, preproc, defines, user-defined literals) + - Conditional includes inside `#ifdef`/`#else`/`#endif` blocks + - System include preservation + - Edge cases: empty source, only comments, nested ifdefs + +## Performance Considerations + +- **AST caching**: Parsed trees cached in `HashMap` to avoid re-parsing +- **Capacity hints**: Output strings pre-allocated with `String::with_capacity` +- **Release profile**: LTO + strip + panic=abort minimizes binary size +- **No unnecessary allocation**: `CompressorState` keeps a reusable buffer diff --git a/.claude/project-tracker/modules/cli.md b/.claude/project-tracker/modules/cli.md new file mode 100644 index 0000000..2a659f3 --- /dev/null +++ b/.claude/project-tracker/modules/cli.md @@ -0,0 +1,47 @@ +# Module: texpand-cli + +**Path**: `texpand-cli/` +**Type**: Binary crate (CLI frontend) +**Entry**: `src/main.rs` — `fn main()` + +## Responsibility + +Provides the command-line interface for texpand. Reads files from the local filesystem via `FsResolver`, handles config from `~/.config/texpand.toml`, and manages output (stdout, file, clipboard). + +## Key Components + +### `FsResolver` (main.rs:54-89) +Implementation of `FileResolver` using `std::fs`: +- Absolute paths: resolved directly via `canonicalize()` +- Relative paths: first checked relative to includer directory, then against configured `include_paths` +- `read_content()`: delegates to `std::fs::read_to_string()` + +### CLI Parser (main.rs:15-48) +Clap derive-based parser with flags: +- `-c`/`--compress` and `--no-compress` — overrides config +- `-i`/`--include` — search paths (repeatable, overrides config) +- `-o`/`--output` — file output +- `-C`/`--clipboard` — clipboard output (mutually exclusive with `-o`) +- `--config` — custom config path +- Stdin support via `-` as input path + +### Clipboard (main.rs:91-118) +- **Linux**: forks a child process that holds the clipboard data alive (arboard clipboard vanishes when process exits) +- **Other platforms**: direct `set_text()` call + +### Config (config.rs) +`TexpandConfig` deserialized from TOML: +- `include_paths: Vec` +- `default_compress: bool` +- Located at `~/.config/texpand.toml` (respects `XDG_CONFIG_HOME`) + +## Dependencies + +| Crate | Usage | +|-------|-------| +| `texpand-core` | Core expansion library | +| `clap 4` | CLI argument parsing | +| `serde 1` | Config deserialization | +| `toml 1` | Config file parsing | +| `arboard 3` | Clipboard access | +| `nix 0` | Linux fork syscall | diff --git a/.claude/project-tracker/modules/core.md b/.claude/project-tracker/modules/core.md new file mode 100644 index 0000000..fb89219 --- /dev/null +++ b/.claude/project-tracker/modules/core.md @@ -0,0 +1,39 @@ +# Module: texpand-core + +**Path**: `texpand-core/` +**Type**: Library crate (I/O-free core) +**Entry**: `src/lib.rs` — re-exports 4 public modules + +## Responsibility + +Central processing engine for C/C++ include expansion. Must never call `std::fs` or `std::io` directly — all file I/O goes through the `FileResolver` trait. + +## Modules + +| Module | File | Lines | Key Items | +|--------|------|-------|-----------| +| `resolver` | `resolver.rs` | 26 | `FileResolver` trait | +| `parser` | `parser.rs` | 131 | `parse_source()`, `Include`, `extract_all_includes()` | +| `expander` | `expander.rs` | 525 | `expand()`, `ExpandOptions`, `PreprocContext`, `ExpandState` | +| `compressor` | `compressor.rs` | 552 | `CompressorState`, `compress()`, `compress_stripped()` | + +## Dependencies + +``` +tree-sitter 0.24 +tree-sitter-cpp 0.23 (C/C++ grammar for preproc-aware AST) +serde 1 + derive (for ExpandResult serialization) +anyhow 1 (error propagation) +``` + +## Unique Patterns + +- **PreprocContext dedup**: The `completed` set is `HashSet<(PathBuf, PreprocContext)>` — files are NOT deduplicated globally, only within the same preprocessor conditional context. This is the core correctness guarantee. +- **Trait-based I/O isolation**: `FileResolver` is the only way data enters the core. The trait has exactly two methods: `resolve()` (path resolution) and `read_content()` (file content). +- **Reusable CompressorState**: The state machine tracks `prev_last` char for identifier spacing, `compound_depth` for preproc nesting, and `body_nl_counter` for body newline insertion. + +## Tests + +- Unit tests inline in each module under `#[cfg(test)]` +- Integration tests in `tests/` using `FixtureResolver` (in-memory file map) +- Test data in `fixtures/` (real C/C++ files for CLI e2e) diff --git a/.claude/project-tracker/modules/vscode-extension.md b/.claude/project-tracker/modules/vscode-extension.md new file mode 100644 index 0000000..00a0606 --- /dev/null +++ b/.claude/project-tracker/modules/vscode-extension.md @@ -0,0 +1,88 @@ +# Module: texpand-vscode + +**Path**: `texpand-vscode/` +**Type**: WASM binary + TypeScript extension +**Entry**: Rust `src/main.rs`, TypeScript `extension/src/extension.ts` + +## Responsibility + +VSCode extension that provides expand-in-place and expand-to-clipboard functionality for C/C++ files. Runs the core Rust engine as a WASI process — no native binary dependency. + +## Rust WASM Layer (`src/main.rs`) + +WASI process entry point. Reads configuration from environment variables: + +| Env Var | Purpose | +|---------|---------| +| `TEXPAND_ENTRY_PATH` | Entry source file path | +| `TEXPAND_COMPRESS` | `"true"` to enable compression | +| `TEXPAND_INCLUDE_PATHS` | Comma-separated include search paths | + +**WasiFsResolver**: Implements `FileResolver` using WASI filesystem access. Resolves includes relative to the includers directory, then against configured prefix paths. + +**Output**: Writes JSON to stdout: +- Success: `{ "success": true, "data": "" }` +- Error: `{ "success": false, "error": "" }` + +## TypeScript Extension (`extension/src/`) + +### Files +| File | Purpose | +|------|---------| +| `extension.ts` | VSCode activation, command registration, result handling | +| `wasm.ts` | WASI process lifecycle management | + +### Activation +Triggers on `onLanguage:c` and `onLanguage:cpp` events. + +### Commands +| Command | Behavior | +|---------|----------| +| `texpand.expandDefault` | Uses `texpand.outputMode` setting | +| `texpand.expandAndCopy` | Forces clipboard output | +| `texpand.expandToNewFile` | Creates `*.expanded.cpp` file | + +### WASM Lifecycle (`wasm.ts`) +1. Resolves WASM binary path relative to extension +2. Creates `WasiProcess` with mapped workspace directories +3. Sets environment variables from extension settings +4. Reads stdout until process exits +5. Parses JSON result and handles errors + +### Settings (contributes to `package.json`) +| Key | Type | Default | +|-----|------|---------| +| `texpand.includePaths` | `string[]` | `["./"]` | +| `texpand.defaultCompression` | `boolean` | `false` | +| `texpand.outputMode` | `clipboard` or `newFile` | `"clipboard"` | +| `texpand.saveBeforeExpansion` | `boolean` | `true` | + +## Build Pipeline + +``` +Cargo.toml + ↓ cargo build --target wasm32-wasip1 --release +WASM binary (target/wasm32-wasip1/release/) + ↓ wasm-opt +Optimized WASM (pkg/texpand-vscode.wasm) + ↓ esbuild bundling +dist/extension.js + ↓ vsce package +texpand-vscode-*.vsix +``` + +## Dependencies + +### Rust +| Crate | Usage | +|-------|-------| +| `texpand-core` | Core expansion | +| `serde` + `serde_json` | JSON output | +| `anyhow` | Error handling | + +### TypeScript +| Package | Usage | +|---------|-------| +| `@vscode/wasm-wasi` | WASI process host | +| `@vscode/vsce` | VSIX packaging | +| `esbuild` | TypeScript bundling | diff --git a/.claude/project-tracker/progress.md b/.claude/project-tracker/progress.md new file mode 100644 index 0000000..aba340d --- /dev/null +++ b/.claude/project-tracker/progress.md @@ -0,0 +1,47 @@ +# Progress + +## Current Phase + +v0.2.0 — Stable release with dual CLI + VSCode support. + +## Completed Features + +- [x] Tree-sitter based C/C++ parsing with preprocessor support +- [x] Local `#include "..."` expansion via BFS with cycle detection +- [x] System `#include <...>` preservation in output +- [x] `#pragma once` stripping during expansion +- [x] Context-sensitive dedup: same file in different `#ifdef` branches re-expanded correctly +- [x] Token-level semantic-safe compression (comment removal, identifier spacing, preproc newlines) +- [x] User-defined literal preservation in compressor (`123_km` stays intact) +- [x] `#define` space preservation (macro name to replacement separation) +- [x] CLI frontend with clap (stdin, file output, clipboard, config file) +- [x] Linux clipboard fork daemon for paste persistence +- [x] VSCode extension via WASM-WASI (3 commands, virtual filesystem, l10n) +- [x] Cross-platform CI (5 targets) + VSCode VSIX packaging +- [x] Config file (`~/.config/texpand.toml`) with include paths +- [x] l10n support with Chinese locale + +## Known Issues & Technical Debt + +- No `-isystem` vs `-I` distinction in include resolution +- No diagnostic output mode (e.g., `--verbose` showing which files were expanded) +- Compression edge: some C++ constructs may need manual review (e.g., string literal concatenation) +- No benchmark suite for expansion performance +- Test fixtures are hand-written — could benefit from fuzzing + +## Roadmap + +### Near-term (v0.3.0) +- [ ] Support for `-isystem` include paths (system vs local include resolution) +- [ ] Verbose/diagnostic output mode +- [ ] Recursive directory scanning for include paths + +### Medium-term +- [ ] Support for CMake `compile_commands.json` include path extraction +- [ ] `#pragma once` vs include guard heuristic detection +- [ ] WASM size optimization + +### Future +- [ ] Language server protocol (LSP) integration +- [ ] Online Judge auto-submit feature +- [ ] Emacs integration diff --git a/.claude/project-tracker/stack.md b/.claude/project-tracker/stack.md new file mode 100644 index 0000000..d2fa8c7 --- /dev/null +++ b/.claude/project-tracker/stack.md @@ -0,0 +1,49 @@ +# Technology Stack + +## Language & Runtime + +| Layer | Technology | Version | Rationale | +|-------|-----------|---------|-----------| +| Core library | Rust | edition 2024 | Performance, safety, WASM target support | +| CLI binary | Rust | same | No runtime overhead, easy distribution | +| VSCode extension | TypeScript + WASM | Node 22 | WASM-WASI integration, no native binary needed | + +## Key Frameworks & Libraries + +### texpand-core + +| Library | Purpose | Why | +|---------|---------|-----| +| `tree-sitter` 0.24 | C/C++ parsing | Incremental, robust AST generation for real-world C/C++ code | +| `tree-sitter-cpp` 0.23 | C/C++ grammar | Supports preprocessor directives as AST nodes (essential for include analysis) | +| `serde` 1 | Serialization | Result JSON for VSCode frontend | + +### texpand-cli + +| Library | Purpose | Why | +|---------|---------|-----| +| `clap` 4 | CLI argument parsing | Derive-based, compile-time checks, subcommand support | +| `toml` 1 | Config file parsing | Standard format for `~/.config/texpand.toml` | +| `arboard` 3 | Clipboard access | Cross-platform clipboard (forks on Linux for persistence) | +| `nix` 0 | Fork syscall | Linux clipboard daemon forking on Linux | + +### texpand-vscode + +| Library | Purpose | Why | +|---------|---------|-----| +| `serde_json` 1 | JSON output | Structured result for TypeScript host | + +(VSCode side uses `@vscode/wasm-wasi` for WASM process lifecycle and `esbuild` for bundling.) + +## Storage + +- **Config file**: `~/.config/texpand.toml` (TOML) — optional, CLI only +- **VSCode settings**: `settings.json` under `texpand.*` keys — extension side +- No database layer — the tool operates on ephemeral source files + +## Infrastructure + +- **CI**: GitHub Actions (push + PR triggers) +- **Release**: GitHub Releases with cross-platform CLI binaries + `.vsix` +- **WASI SDK**: wasi-sdk 33 for WASM compilation (wasm32-wasip1 target) +- **Cross-compilation**: gcc-aarch64-linux-gnu for ARM64 Linux builds diff --git a/.claude/project-tracker/toolchain.md b/.claude/project-tracker/toolchain.md new file mode 100644 index 0000000..cafa2ba --- /dev/null +++ b/.claude/project-tracker/toolchain.md @@ -0,0 +1,81 @@ +# Toolchain + +## Build System + +Cargo workspace with 3 members. Release profile: LTO + panic=abort + strip. + +```bash +# Build all crates +cargo build --workspace + +# Build specific crate +cargo build -p texpand-core +cargo build -p texpand-cli +cargo build -p texpand-vscode --target wasm32-wasip1 --release +``` + +## Linting & Formatting + +- **Formatter**: `cargo fmt --all` (rustfmt, 100-char line width, 4-space indent) +- **Linter**: `cargo clippy --workspace -- -D warnings` (deny warnings as errors) + +## Testing + +- **Framework**: built-in `#[test]` with `#[cfg(test)]` modules +- **Unit tests**: inline in each source file under `mod tests` +- **Integration tests**: `texpand-core/tests/` — use `FixtureResolver` (in-memory file map) +- **Coverage target**: not explicitly configured (no coverage CI step) +- **Run**: `cargo test --workspace` + +``` +texpand-core/tests/ +├── common.rs # Shared test utilities (FixtureResolver) +├── test_basic_expansion.rs +├── test_circular_dep.rs +├── test_compression.rs +├── test_conditional_includes.rs +├── test_edge_cases.rs +└── test_system_include.rs +``` + +## CI/CD Pipeline + +### CI (`.github/workflows/ci.yml`) +Triggers on push to any branch and PRs: + +| Job | Tool | Purpose | +|-----|------|---------| +| `check` | `cargo check` | Verify compilation | +| `fmt` | `cargo fmt -- --check` | Formatting compliance | +| `clippy` | `cargo clippy -- -D warnings` | Lint enforcement | +| `test` | `cargo test --workspace` | All tests | +| `typecheck` | `tsc --noEmit` | VSCode extension types | + +### Release (`.github/workflows/release.yml`) +Triggers on `v*` tag push: + +- **CLI binaries**: 5 targets (x86_64 Linux/ARM64 Linux/x86_64 macOS/ARM64 macOS/x86_64 Windows) +- **VSIX package**: WASM build + esbuild bundle + `vsce package` +- **GitHub Release**: auto-generated release notes, all artifacts attached + +## Dev Environment Prerequisites + +| Tool | Purpose | +|------|---------| +| Rust stable toolchain | Core compilation | +| wasm32-wasip1 target | VSCode WASM build | +| wasi-sdk 33 | WASM C/C++ linker | +| Node.js 22 | VSCode extension build | +| wasm-opt | WASM binary optimization | +| cargo-llvm-cov (optional) | Coverage reporting | + +## Environment Variables + +| Variable | Used By | Purpose | +|----------|---------|---------| +| `TEXPAND_ENTRY_PATH` | texpand-vscode WASM | Entry source file path | +| `TEXPAND_COMPRESS` | texpand-vscode WASM | Enable compression flag | +| `TEXPAND_INCLUDE_PATHS` | texpand-vscode WASM | Comma-separated include search paths | +| `XDG_CONFIG_HOME` | texpand-cli | Config file location override | +| `CC_wasm32_wasip1` | .cargo/config.toml | WASI SDK clang path | +| `CXX_wasm32_wasip1` | .cargo/config.toml | WASI SDK clang++ path | diff --git a/texpand-vscode/extension/README.md b/texpand-vscode/extension/README.md index cb7acac..debe429 100644 --- a/texpand-vscode/extension/README.md +++ b/texpand-vscode/extension/README.md @@ -53,6 +53,10 @@ None yet. ## Release Notes +### 0.2.0 + +- Feature: add a configuration `saveBeforeExpansion` to control whether the current file would be saved before expansion. Default value is `true`. + ### 0.1.2 - Fix: user-defined literals (e.g. `123_km`) no longer get a spurious space inserted during compression. diff --git a/texpand-vscode/extension/README.zh-CN.md b/texpand-vscode/extension/README.zh-CN.md index b9b0df7..512b859 100644 --- a/texpand-vscode/extension/README.zh-CN.md +++ b/texpand-vscode/extension/README.zh-CN.md @@ -53,6 +53,10 @@ ## 更新日志 +### 0.2.0 + +- 特性:增加了一个配置项 `saveBeforeExpansion`,用于控制是否在展开代码前保存当前文件。默认值为 `true`。 + ### 0.1.2 - 修复:用户定义字面量(例如 `123_km`)在压缩时不再被错误插入空格。 diff --git a/texpand-vscode/extension/l10n/bundle.l10n.json b/texpand-vscode/extension/l10n/bundle.l10n.json new file mode 100644 index 0000000..bf3a7a3 --- /dev/null +++ b/texpand-vscode/extension/l10n/bundle.l10n.json @@ -0,0 +1,28 @@ +{ + "Process exited with code {0}": "Process exited with code {0}", + "Unknown expansion error": "Unknown expansion error", + "$(file-code) Texpand": "$(file-code) Texpand", + "Click to configure Texpand settings": "Click to configure Texpand settings", + "Texpand: No active editor": "Texpand: No active editor", + "Texpand: Only C/C++ files are supported": "Texpand: Only C/C++ files are supported", + "Texpand: Expanding...": "Texpand: Expanding...", + "Texpand: Expanded code copied to clipboard": "Texpand: Expanded code copied to clipboard", + "Texpand: Expanded to {0}": "Texpand: Expanded to {0}", + "Open": "Open", + "Texpand: Circular dependency detected — {0}": "Texpand: Circular dependency detected — {0}", + "Texpand: {0}": "Texpand: {0}", + "Currently ON — code will be minified": "Currently ON — code will be minified", + "Currently OFF — code will be formatted normally": "Currently OFF — code will be formatted normally", + "Currently: Copy to clipboard": "Currently: Copy to clipboard", + "Currently: Write to .expanded.cpp file": "Currently: Write to .expanded.cpp file", + "Include Paths: {0} path(s)": "Include Paths: {0} path(s)", + "Select a Texpand setting to change": "Select a Texpand setting to change", + "Compression: On": "Compression: On", + "Compression: Off": "Compression: Off", + "Output Mode: Clipboard": "Output Mode: Clipboard", + "Output Mode: New File": "Output Mode: New File", + "Texpand: Compression set to On": "Texpand: Compression set to On", + "Texpand: Compression set to Off": "Texpand: Compression set to Off", + "Texpand: Output mode set to Clipboard": "Texpand: Output mode set to Clipboard", + "Texpand: Output mode set to New File": "Texpand: Output mode set to New File" +} \ No newline at end of file diff --git a/texpand-vscode/extension/l10n/bundle.l10n.zh-cn.json b/texpand-vscode/extension/l10n/bundle.l10n.zh-cn.json new file mode 100644 index 0000000..2ff04c5 --- /dev/null +++ b/texpand-vscode/extension/l10n/bundle.l10n.zh-cn.json @@ -0,0 +1,28 @@ +{ + "$(file-code) Texpand": "$(file-code) Texpand", + "Click to configure Texpand settings": "点击配置 Texpand 设置", + "Texpand: No active editor": "Texpand: 没有活动的编辑器", + "Texpand: Only C/C++ files are supported": "Texpand: 仅支持 C/C++ 文件", + "Texpand: Expanding...": "Texpand: 正在展开...", + "Texpand: Expanded code copied to clipboard": "Texpand: 已复制展开后的代码到剪贴板", + "Texpand: Expanded to {0}": "Texpand: 已展开到 {0}", + "Open": "打开", + "Texpand: Circular dependency detected — {0}": "Texpand: 检测到循环依赖 — {0}", + "Texpand: {0}": "Texpand: {0}", + "Compression: On": "压缩:开", + "Compression: Off": "压缩:关", + "Currently ON — code will be minified": "当前为开 — 代码将被压缩", + "Currently OFF — code will be formatted normally": "当前为关 — 代码将保持正常格式", + "Output Mode: Clipboard": "输出模式:剪贴板", + "Output Mode: New File": "输出模式:新文件", + "Currently: Copy to clipboard": "当前:复制到剪贴板", + "Currently: Write to .expanded.cpp file": "当前:写入到 .expanded.cpp 文件", + "Include Paths: {0} path(s)": "包含路径:{0} 个", + "Select a Texpand setting to change": "选择要更改的 Texpand 设置", + "Texpand: Compression set to On": "Texpand: 压缩已开启", + "Texpand: Compression set to Off": "Texpand: 压缩已关闭", + "Texpand: Output mode set to Clipboard": "Texpand: 输出模式已设为剪贴板", + "Texpand: Output mode set to New File": "Texpand: 输出模式已设为新文件", + "Process exited with code {0}": "进程退出,退出码 {0}", + "Unknown expansion error": "未知的展开错误" +} diff --git a/texpand-vscode/extension/package-lock.json b/texpand-vscode/extension/package-lock.json index ecb9cfa..6b2a6b9 100644 --- a/texpand-vscode/extension/package-lock.json +++ b/texpand-vscode/extension/package-lock.json @@ -1,12 +1,12 @@ { "name": "texpand-vscode", - "version": "0.1.0", + "version": "0.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "texpand-vscode", - "version": "0.1.0", + "version": "0.2.0", "dependencies": { "@vscode/vsce": "^3.9.1", "@vscode/wasm-wasi": "^1.0.0" @@ -14,6 +14,7 @@ "devDependencies": { "@types/node": "22.x", "@types/vscode": "^1.85.0", + "@vscode/l10n-dev": "^0.0.35", "esbuild": "^0.28.0", "npm-run-all2": "^8.0.4", "typescript": "^5.9.3", @@ -38,6 +39,41 @@ "@azu/format-text": "^1.0.1" } }, + "node_modules/@azure-rest/ai-translation-text": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/@azure-rest/ai-translation-text/-/ai-translation-text-1.0.1.tgz", + "integrity": "sha512-lUs1FfBXjik6EReUEYP1ogkhaSPHZdUV+EB215y7uejuyHgG1RXD2aLsqXQrluZwXcLMdN+bTzxylKBc5xDhgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure-rest/core-client": "^2.3.1", + "@azure/core-auth": "^1.9.0", + "@azure/core-rest-pipeline": "^1.18.0", + "@azure/logger": "^1.1.4", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure-rest/core-client": { + "version": "2.6.0", + "resolved": "https://registry.npmmirror.com/@azure-rest/core-client/-/core-client-2.6.0.tgz", + "integrity": "sha512-iuFKDm8XPzNxPfRjhyU5/xKZmcRDzSuEghXDHHk4MjBV/wFL34GmYVBZnn9wmuoLBeS1qAw9ceMdaeJBPcB1QQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.10.0", + "@azure/core-rest-pipeline": "^1.22.0", + "@azure/core-tracing": "^1.3.0", + "@typespec/ts-http-runtime": "^0.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/@azure/abort-controller": { "version": "2.1.2", "resolved": "https://registry.npmmirror.com/@azure/abort-controller/-/abort-controller-2.1.2.tgz", @@ -704,6 +740,17 @@ "node": ">= 8" } }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmmirror.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@secretlint/config-creator": { "version": "10.2.2", "resolved": "https://registry.npmmirror.com/@secretlint/config-creator/-/config-creator-10.2.2.tgz", @@ -1033,6 +1080,28 @@ "node": ">=20.0.0" } }, + "node_modules/@vscode/l10n-dev": { + "version": "0.0.35", + "resolved": "https://registry.npmmirror.com/@vscode/l10n-dev/-/l10n-dev-0.0.35.tgz", + "integrity": "sha512-s6uzBXsVDSL69Z85HSqpc5dfKswQkeucY8L00t1TWzGalw7wkLQUKMRwuzqTq+AMwQKrRd7Po14cMoTcd11iDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure-rest/ai-translation-text": "^1.0.0-beta.1", + "debug": "^4.3.4", + "deepmerge-json": "^1.5.0", + "glob": "^10.0.0", + "markdown-it": "^14.0.0", + "node-html-markdown": "^1.3.0", + "pseudo-localization": "^2.4.0", + "web-tree-sitter": "^0.20.8", + "xml2js": "^0.5.0", + "yargs": "^17.7.1" + }, + "bin": { + "vscode-l10n-dev": "dist/cli.js" + } + }, "node_modules/@vscode/vsce": { "version": "3.9.1", "resolved": "https://registry.npmmirror.com/@vscode/vsce/-/vsce-3.9.1.tgz", @@ -1317,38 +1386,6 @@ "node": ">=8" } }, - "node_modules/@vscode/vsce/node_modules/linkify-it": { - "version": "5.0.0", - "resolved": "https://registry.npmmirror.com/linkify-it/-/linkify-it-5.0.0.tgz", - "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", - "license": "MIT", - "dependencies": { - "uc.micro": "^2.0.0" - } - }, - "node_modules/@vscode/vsce/node_modules/markdown-it": { - "version": "14.1.1", - "resolved": "https://registry.npmmirror.com/markdown-it/-/markdown-it-14.1.1.tgz", - "integrity": "sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==", - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1", - "entities": "^4.4.0", - "linkify-it": "^5.0.0", - "mdurl": "^2.0.0", - "punycode.js": "^2.3.1", - "uc.micro": "^2.1.0" - }, - "bin": { - "markdown-it": "bin/markdown-it.mjs" - } - }, - "node_modules/@vscode/vsce/node_modules/mdurl": { - "version": "2.0.0", - "resolved": "https://registry.npmmirror.com/mdurl/-/mdurl-2.0.0.tgz", - "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", - "license": "MIT" - }, "node_modules/@vscode/vsce/node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-7.2.0.tgz", @@ -1361,25 +1398,6 @@ "node": ">=8" } }, - "node_modules/@vscode/vsce/node_modules/uc.micro": { - "version": "2.1.0", - "resolved": "https://registry.npmmirror.com/uc.micro/-/uc.micro-2.1.0.tgz", - "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", - "license": "MIT" - }, - "node_modules/@vscode/vsce/node_modules/xml2js": { - "version": "0.5.0", - "resolved": "https://registry.npmmirror.com/xml2js/-/xml2js-0.5.0.tgz", - "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==", - "license": "MIT", - "dependencies": { - "sax": ">=0.6.0", - "xmlbuilder": "~11.0.0" - }, - "engines": { - "node": ">=4.0.0" - } - }, "node_modules/@vscode/vsce/node_modules/yauzl": { "version": "3.3.0", "resolved": "https://registry.npmmirror.com/yauzl/-/yauzl-3.3.0.tgz", @@ -1901,6 +1919,16 @@ "node": ">=4.0.0" } }, + "node_modules/deepmerge-json": { + "version": "1.5.0", + "resolved": "https://registry.npmmirror.com/deepmerge-json/-/deepmerge-json-1.5.0.tgz", + "integrity": "sha512-jZRrDmBKjmGcqMFEUJ14FjMJwm05Qaked+1vxaALRtF0UAl7lPU8OLWXFxvoeg3jbQM249VPFVn8g2znaQkEtA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/default-browser": { "version": "5.5.0", "resolved": "https://registry.npmmirror.com/default-browser/-/default-browser-5.5.0.tgz", @@ -2029,6 +2057,13 @@ "node": ">= 0.4" } }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmmirror.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, "node_modules/ecdsa-sig-formatter": { "version": "1.0.11", "resolved": "https://registry.npmmirror.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", @@ -2272,6 +2307,16 @@ "node": ">=8" } }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmmirror.com/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "license": "BSD-3-Clause", + "bin": { + "flat": "cli.js" + } + }, "node_modules/foreground-child": { "version": "3.3.1", "resolved": "https://registry.npmmirror.com/foreground-child/-/foreground-child-3.3.1.tgz", @@ -2406,6 +2451,16 @@ "node": ">= 0.4" } }, + "node_modules/get-stdin": { + "version": "7.0.0", + "resolved": "https://registry.npmmirror.com/get-stdin/-/get-stdin-7.0.0.tgz", + "integrity": "sha512-zRKcywvrXlXsA0v0i9Io4KDRaAw7+a1ZpjRwl9Wox8PFlVCCHra7E9c4kqXCoCM9nR5tBkaTTZRBoCm60bFqTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/github-from-package": { "version": "0.0.0", "resolved": "https://registry.npmmirror.com/github-from-package/-/github-from-package-0.0.0.tgz", @@ -2413,6 +2468,27 @@ "license": "MIT", "optional": true }, + "node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmmirror.com/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/glob-parent": { "version": "5.1.2", "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz", @@ -2425,6 +2501,115 @@ "node": ">= 6" } }, + "node_modules/glob/node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmmirror.com/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/glob/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/glob/node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmmirror.com/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/glob/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob/node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmmirror.com/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/globby": { "version": "14.1.0", "resolved": "https://registry.npmmirror.com/globby/-/globby-14.1.0.tgz", @@ -2502,6 +2687,16 @@ "node": ">= 0.4" } }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, "node_modules/hosted-git-info": { "version": "4.1.0", "resolved": "https://registry.npmmirror.com/hosted-git-info/-/hosted-git-info-4.1.0.tgz", @@ -2892,6 +3087,15 @@ "node": ">=6" } }, + "node_modules/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "license": "MIT", + "dependencies": { + "uc.micro": "^2.0.0" + } + }, "node_modules/lodash": { "version": "4.18.1", "resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.18.1.tgz", @@ -2958,6 +3162,23 @@ "node": ">=10" } }, + "node_modules/markdown-it": { + "version": "14.1.1", + "resolved": "https://registry.npmmirror.com/markdown-it/-/markdown-it-14.1.1.tgz", + "integrity": "sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1", + "entities": "^4.4.0", + "linkify-it": "^5.0.0", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -2967,6 +3188,12 @@ "node": ">= 0.4" } }, + "node_modules/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", + "license": "MIT" + }, "node_modules/memorystream": { "version": "0.3.1", "resolved": "https://registry.npmmirror.com/memorystream/-/memorystream-0.3.1.tgz", @@ -3182,6 +3409,30 @@ } } }, + "node_modules/node-html-markdown": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/node-html-markdown/-/node-html-markdown-1.3.0.tgz", + "integrity": "sha512-OeFi3QwC/cPjvVKZ114tzzu+YoR+v9UXW5RwSXGUqGb0qCl0DvP406tzdL7SFn8pZrMyzXoisfG2zcuF9+zw4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "node-html-parser": "^6.1.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/node-html-parser": { + "version": "6.1.13", + "resolved": "https://registry.npmmirror.com/node-html-parser/-/node-html-parser-6.1.13.tgz", + "integrity": "sha512-qIsTMOY4C/dAa5Q5vsobRpOOvPfC4pB61UVW2uSwZNUp0QU/jCekTal1vMmbO0DgdHeLUJpv/ARmDqErVxA3Sg==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-select": "^5.1.0", + "he": "1.2.0" + } + }, "node_modules/node-sarif-builder": { "version": "3.4.0", "resolved": "https://registry.npmmirror.com/node-sarif-builder/-/node-sarif-builder-3.4.0.tgz", @@ -3589,6 +3840,36 @@ "node": ">=10" } }, + "node_modules/pseudo-localization": { + "version": "2.4.0", + "resolved": "https://registry.npmmirror.com/pseudo-localization/-/pseudo-localization-2.4.0.tgz", + "integrity": "sha512-ISYMOKY8+f+PmiXMFw2y6KLY74LBrv/8ml/VjjoVEV2k+MS+OJZz7ydciK5ntJwxPrKQPTU1+oXq9Mx2b0zEzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat": "^5.0.2", + "get-stdin": "^7.0.0", + "typescript": "^4.7.4", + "yargs": "^17.2.1" + }, + "bin": { + "pseudo-localization": "bin/pseudo-localize" + } + }, + "node_modules/pseudo-localization/node_modules/typescript": { + "version": "4.9.5", + "resolved": "https://registry.npmmirror.com/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, "node_modules/pump": { "version": "3.0.4", "resolved": "https://registry.npmmirror.com/pump/-/pump-3.0.4.tgz", @@ -4125,6 +4406,45 @@ "node": ">=8" } }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/string-width/node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -4161,6 +4481,30 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/strip-json-comments": { "version": "2.0.1", "resolved": "https://registry.npmmirror.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz", @@ -4452,6 +4796,12 @@ "node": ">=14.17" } }, + "node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "license": "MIT" + }, "node_modules/underscore": { "version": "1.13.8", "resolved": "https://registry.npmmirror.com/underscore/-/underscore-1.13.8.tgz", @@ -4545,6 +4895,13 @@ "wasm-opt": "bin/wasm-opt.js" } }, + "node_modules/web-tree-sitter": { + "version": "0.20.8", + "resolved": "https://registry.npmmirror.com/web-tree-sitter/-/web-tree-sitter-0.20.8.tgz", + "integrity": "sha512-weOVgZ3aAARgdnb220GqYuh7+rZU0Ka9k9yfKtGAzEYMa6GgiCzW9JjQRJyCJakvibQW+dfjJdihjInKuuCAUQ==", + "dev": true, + "license": "MIT" + }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmmirror.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz", @@ -4600,6 +4957,104 @@ "node": ">= 8" } }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmmirror.com/wrappy/-/wrappy-1.0.2.tgz", @@ -4622,6 +5077,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/xml2js": { + "version": "0.5.0", + "resolved": "https://registry.npmmirror.com/xml2js/-/xml2js-0.5.0.tgz", + "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==", + "license": "MIT", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/xmlbuilder": { "version": "11.0.1", "resolved": "https://registry.npmmirror.com/xmlbuilder/-/xmlbuilder-11.0.1.tgz", diff --git a/texpand-vscode/extension/package.json b/texpand-vscode/extension/package.json index 564a40f..bbfa869 100644 --- a/texpand-vscode/extension/package.json +++ b/texpand-vscode/extension/package.json @@ -1,8 +1,9 @@ { "name": "texpand-vscode", - "displayName": "Template Expand", - "description": "C/C++ template expansion tool — expand #include dependencies", - "version": "0.1.2", + "displayName": "%extension.displayName%", + "description": "%extension.description%", + "l10n": "./l10n", + "version": "0.2.0", "publisher": "explodingkonjac", "repository": { "type": "git", @@ -28,22 +29,22 @@ "commands": [ { "command": "texpand.expandDefault", - "title": "Texpand: Expand Current File (Default)", + "title": "%texpand.command.expandDefault.title%", "icon": "$(file-code)" }, { "command": "texpand.expandAndCopy", - "title": "Texpand: Expand and Copy to Clipboard", + "title": "%texpand.command.expandAndCopy.title%", "icon": "$(copy)" }, { "command": "texpand.expandToNewFile", - "title": "Texpand: Expand to New File", + "title": "%texpand.command.expandToNewFile.title%", "icon": "$(new-file)" } ], "configuration": { - "title": "Texpand", + "title": "%texpand.configuration.title%", "properties": { "texpand.includePaths": { "type": "array", @@ -53,12 +54,12 @@ "default": [ "./" ], - "description": "Local template header search paths. Supports workspace-relative or absolute paths." + "description": "%texpand.configuration.includePaths.description%" }, "texpand.defaultCompression": { "type": "boolean", "default": false, - "description": "Whether to enable token-level code compression by default." + "description": "%texpand.configuration.defaultCompression.description%" }, "texpand.outputMode": { "type": "string", @@ -68,17 +69,22 @@ ], "default": "clipboard", "enumDescriptions": [ - "Copy expanded code to clipboard", - "Write expanded code to a new .expanded.cpp file" + "%texpand.configuration.outputMode.enumDescription.clipboard%", + "%texpand.configuration.outputMode.enumDescription.newFile%" ], - "description": "Output destination for expanded code." + "description": "%texpand.configuration.outputMode.description%" + }, + "texpand.saveBeforeExpansion": { + "type": "boolean", + "default": true, + "description": "%texpand.configuration.saveBeforeExpansion.description%" } } }, "submenus": [ { "id": "texpand.contextMenu", - "label": "Texpand" + "label": "%texpand.submenu.label%" } ], "menus": { @@ -117,11 +123,13 @@ "typecheck": "tsc --noEmit -p ./", "watch": "npm run esbuild:base -- --watch=forever", "vscode:prepublish": "npm-run-all wasm:build build:ext", - "package": "vsce package" + "package": "vsce package", + "l10n:export": "npx @vscode/l10n-dev export --outDir ./l10n ./src" }, "devDependencies": { "@types/node": "22.x", "@types/vscode": "^1.85.0", + "@vscode/l10n-dev": "^0.0.35", "esbuild": "^0.28.0", "npm-run-all2": "^8.0.4", "typescript": "^5.9.3", diff --git a/texpand-vscode/extension/package.nls.json b/texpand-vscode/extension/package.nls.json new file mode 100644 index 0000000..bb2a3d0 --- /dev/null +++ b/texpand-vscode/extension/package.nls.json @@ -0,0 +1,15 @@ +{ + "extension.displayName": "Template Expand", + "extension.description": "C/C++ template expansion tool — expand #include dependencies", + "texpand.command.expandDefault.title": "Texpand: Expand Current File (Default)", + "texpand.command.expandAndCopy.title": "Texpand: Expand and Copy to Clipboard", + "texpand.command.expandToNewFile.title": "Texpand: Expand to New File", + "texpand.configuration.title": "Texpand", + "texpand.configuration.includePaths.description": "Local template header search paths. Supports workspace-relative or absolute paths.", + "texpand.configuration.defaultCompression.description": "Whether to enable token-level code compression by default.", + "texpand.configuration.outputMode.description": "Output destination for expanded code.", + "texpand.configuration.outputMode.enumDescription.clipboard": "Copy expanded code to clipboard", + "texpand.configuration.outputMode.enumDescription.newFile": "Write expanded code to a new .expanded.cpp file", + "texpand.configuration.saveBeforeExpansion.description": "Whether to save the current file before expanding.", + "texpand.submenu.label": "Texpand" +} diff --git a/texpand-vscode/extension/package.nls.qps-ploc.json b/texpand-vscode/extension/package.nls.qps-ploc.json new file mode 100644 index 0000000..ad4bcdd --- /dev/null +++ b/texpand-vscode/extension/package.nls.qps-ploc.json @@ -0,0 +1,15 @@ +{ + "extension.displayName": "Ŧḗḗḿƥŀȧȧŧḗḗ ḖḖẋƥȧȧƞḓ", + "extension.description": "Ƈ/Ƈ++ ŧḗḗḿƥŀȧȧŧḗḗ ḗḗẋƥȧȧƞşīǿǿƞ ŧǿǿǿǿŀ — ḗḗẋƥȧȧƞḓ #īƞƈŀŭŭḓḗḗ ḓḗḗƥḗḗƞḓḗḗƞƈīḗḗş", + "texpand.command.expandDefault.title": "Ŧḗḗẋƥȧȧƞḓ: ḖḖẋƥȧȧƞḓ Ƈŭŭřřḗḗƞŧ Ƒīŀḗḗ (Ḓḗḗƒȧȧŭŭŀŧ)", + "texpand.command.expandAndCopy.title": "Ŧḗḗẋƥȧȧƞḓ: ḖḖẋƥȧȧƞḓ ȧȧƞḓ Ƈǿǿƥẏ ŧǿǿ Ƈŀīƥƀǿǿȧȧřḓ", + "texpand.command.expandToNewFile.title": "Ŧḗḗẋƥȧȧƞḓ: ḖḖẋƥȧȧƞḓ ŧǿǿ Ƞḗḗẇ Ƒīŀḗḗ", + "texpand.configuration.title": "Ŧḗḗẋƥȧȧƞḓ", + "texpand.configuration.includePaths.description": "Ŀǿǿƈȧȧŀ ŧḗḗḿƥŀȧȧŧḗḗ ħḗḗȧȧḓḗḗř şḗḗȧȧřƈħ ƥȧȧŧħş. Şŭŭƥƥǿǿřŧş ẇǿǿřķşƥȧȧƈḗḗ-řḗḗŀȧȧŧīṽḗḗ ǿǿř ȧȧƀşǿǿŀŭŭŧḗḗ ƥȧȧŧħş.", + "texpand.configuration.defaultCompression.description": "Ẇħḗḗŧħḗḗř ŧǿǿ ḗḗƞȧȧƀŀḗḗ ŧǿǿķḗḗƞ-ŀḗḗṽḗḗŀ ƈǿǿḓḗḗ ƈǿǿḿƥřḗḗşşīǿǿƞ ƀẏ ḓḗḗƒȧȧŭŭŀŧ.", + "texpand.configuration.outputMode.description": "ǾǾŭŭŧƥŭŭŧ ḓḗḗşŧīƞȧȧŧīǿǿƞ ƒǿǿř ḗḗẋƥȧȧƞḓḗḗḓ ƈǿǿḓḗḗ.", + "texpand.configuration.outputMode.enumDescription.clipboard": "Ƈǿǿƥẏ ḗḗẋƥȧȧƞḓḗḗḓ ƈǿǿḓḗḗ ŧǿǿ ƈŀīƥƀǿǿȧȧřḓ", + "texpand.configuration.outputMode.enumDescription.newFile": "Ẇřīŧḗḗ ḗḗẋƥȧȧƞḓḗḗḓ ƈǿǿḓḗḗ ŧǿǿ ȧȧ ƞḗḗẇ .ḗḗẋƥȧȧƞḓḗḗḓ.ƈƥƥ ƒīŀḗḗ", + "texpand.configuration.saveBeforeExpansion.description": "Ẇħḗḗŧħḗḗř ŧǿǿ şȧȧṽḗḗ ŧħḗḗ ƈŭŭřřḗḗƞŧ ƒīŀḗḗ ƀḗḗƒǿǿřḗḗ ḗḗẋƥȧȧƞḓīƞɠ.", + "texpand.submenu.label": "Ŧḗḗẋƥȧȧƞḓ" +} \ No newline at end of file diff --git a/texpand-vscode/extension/package.nls.zh-cn.json b/texpand-vscode/extension/package.nls.zh-cn.json new file mode 100644 index 0000000..2c8c0d8 --- /dev/null +++ b/texpand-vscode/extension/package.nls.zh-cn.json @@ -0,0 +1,15 @@ +{ + "extension.displayName": "Template Expand", + "extension.description": "C/C++ 模板展开工具 — 展开 #include 依赖", + "texpand.command.expandDefault.title": "Texpand: 展开当前文件(默认模式)", + "texpand.command.expandAndCopy.title": "Texpand: 展开并复制到剪贴板", + "texpand.command.expandToNewFile.title": "Texpand: 展开到新文件", + "texpand.configuration.title": "Texpand", + "texpand.configuration.includePaths.description": "本地模板头文件搜索路径。支持相对于工作区或绝对路径。", + "texpand.configuration.defaultCompression.description": "是否默认启用代码压缩。", + "texpand.configuration.outputMode.description": "展开后代码的输出方式。", + "texpand.configuration.outputMode.enumDescription.clipboard": "将展开的代码复制到剪贴板", + "texpand.configuration.outputMode.enumDescription.newFile": "将展开的代码写入新的 .expanded.cpp 文件", + "texpand.configuration.saveBeforeExpansion.description": "展开前是否保存当前文件。", + "texpand.submenu.label": "Texpand" +} diff --git a/texpand-vscode/extension/src/extension.ts b/texpand-vscode/extension/src/extension.ts index 1b444f2..f91ac77 100644 --- a/texpand-vscode/extension/src/extension.ts +++ b/texpand-vscode/extension/src/extension.ts @@ -11,8 +11,8 @@ export function activate(context: vscode.ExtensionContext) { const statusBarItem = vscode.window.createStatusBarItem( vscode.StatusBarAlignment.Right, 100, ); - statusBarItem.text = '$(file-code) Texpand'; - statusBarItem.tooltip = 'Click to configure Texpand settings'; + statusBarItem.text = vscode.l10n.t('$(file-code) Texpand'); + statusBarItem.tooltip = vscode.l10n.t('Click to configure Texpand settings'); statusBarItem.command = 'texpand.showConfigQuickPick'; const commands = [ @@ -54,13 +54,13 @@ function updateStatusBar(item: vscode.StatusBarItem): void { async function runExpansion(context: vscode.ExtensionContext, mode: ExpansionMode): Promise { const editor = vscode.window.activeTextEditor; if (!editor) { - vscode.window.showErrorMessage('Texpand: No active editor'); + vscode.window.showErrorMessage(vscode.l10n.t('Texpand: No active editor')); return; } const langId = editor.document.languageId; if (langId !== 'c' && langId !== 'cpp') { - vscode.window.showErrorMessage('Texpand: Only C/C++ files are supported'); + vscode.window.showErrorMessage(vscode.l10n.t('Texpand: Only C/C++ files are supported')); return; } @@ -73,14 +73,18 @@ async function runExpansion(context: vscode.ExtensionContext, mode: ExpansionMod const entryPath = editor.document.uri.fsPath; try { + if (config.get('saveBeforeExpansion', true)) { + await editor.document.save(); + } + const result = await vscode.window.withProgress( - { location: vscode.ProgressLocation.Notification, title: 'Texpand: Expanding...' }, + { location: vscode.ProgressLocation.Notification, title: vscode.l10n.t('Texpand: Expanding...') }, () => expandWithProcess(context, entryPath, { compress, includePaths }), ); if (actualMode === 'clipboard') { await vscode.env.clipboard.writeText(result); - vscode.window.showInformationMessage('Texpand: Expanded code copied to clipboard'); + vscode.window.showInformationMessage(vscode.l10n.t('Texpand: Expanded code copied to clipboard')); } else { const ext = path.extname(entryPath); const base = entryPath.slice(0, -ext.length); @@ -89,8 +93,8 @@ async function runExpansion(context: vscode.ExtensionContext, mode: ExpansionMod await vscode.workspace.fs.writeFile(newUri, Buffer.from(result, 'utf-8')); const action = await vscode.window.showInformationMessage( - `Texpand: Expanded to ${path.basename(newFilePath)}`, - 'Open', + vscode.l10n.t('Texpand: Expanded to {0}', path.basename(newFilePath)), + vscode.l10n.t('Open'), ); if (action === 'Open') { const doc = await vscode.workspace.openTextDocument(newUri); @@ -100,9 +104,9 @@ async function runExpansion(context: vscode.ExtensionContext, mode: ExpansionMod } catch (e) { const msg = e instanceof Error ? e.message : String(e); if (msg.includes('circular dependency')) { - vscode.window.showErrorMessage(`Texpand: Circular dependency detected — ${msg}`); + vscode.window.showErrorMessage(vscode.l10n.t('Texpand: Circular dependency detected — {0}', msg)); } else { - vscode.window.showErrorMessage(`Texpand: ${msg}`); + vscode.window.showErrorMessage(vscode.l10n.t('Texpand: {0}', msg)); } } } @@ -117,21 +121,25 @@ async function showConfigQuickPick(): Promise { const items: vscode.QuickPickItem[] = [ { - label: `$(file-zip) Compression: ${compression ? 'On' : 'Off'}`, - description: compression ? 'Currently ON — code will be minified' : 'Currently OFF — code will be formatted normally', + label: `$(file-zip) ${vscode.l10n.t(compression ? 'Compression: On' : 'Compression: Off')}`, + description: compression + ? vscode.l10n.t('Currently ON — code will be minified') + : vscode.l10n.t('Currently OFF — code will be formatted normally'), }, { - label: `$(output) Output Mode: ${outputMode === 'clipboard' ? 'Clipboard' : 'New File'}`, - description: outputMode === 'clipboard' ? 'Currently: Copy to clipboard' : 'Currently: Write to .expanded.cpp file', + label: `$(output) ${vscode.l10n.t(outputMode === 'clipboard' ? 'Output Mode: Clipboard' : 'Output Mode: New File')}`, + description: outputMode === 'clipboard' + ? vscode.l10n.t('Currently: Copy to clipboard') + : vscode.l10n.t('Currently: Write to .expanded.cpp file'), }, { - label: `$(list-unordered) Include Paths: ${includePaths.length} path(s)`, + label: `$(list-unordered) ${vscode.l10n.t('Include Paths: {0} path(s)', includePaths.length)}`, description: includePaths.join(', '), }, ]; const pick = await vscode.window.showQuickPick(items, { - placeHolder: 'Select a Texpand setting to change', + placeHolder: vscode.l10n.t('Select a Texpand setting to change'), }); if (!pick) return; @@ -141,13 +149,13 @@ async function showConfigQuickPick(): Promise { if (pick === items[0]) { await config.update('defaultCompression', !compression, target); vscode.window.showInformationMessage( - `Texpand: Compression set to ${!compression ? 'On' : 'Off'}`, + vscode.l10n.t(!compression ? 'Texpand: Compression set to On' : 'Texpand: Compression set to Off'), ); } else if (pick === items[1]) { const next = outputMode === 'clipboard' ? 'newFile' : 'clipboard'; await config.update('outputMode', next, target); vscode.window.showInformationMessage( - `Texpand: Output mode set to ${next === 'clipboard' ? 'Clipboard' : 'New File'}`, + vscode.l10n.t(next === 'clipboard' ? 'Texpand: Output mode set to Clipboard' : 'Texpand: Output mode set to New File'), ); } else if (pick === items[2]) { await vscode.commands.executeCommand( diff --git a/texpand-vscode/extension/src/wasm.ts b/texpand-vscode/extension/src/wasm.ts index 6f40fdc..3ab2cca 100644 --- a/texpand-vscode/extension/src/wasm.ts +++ b/texpand-vscode/extension/src/wasm.ts @@ -111,12 +111,12 @@ export async function expandWithProcess( } if (exitCode !== 0) { - throw new Error(stderr || `Process exited with code ${exitCode}`); + throw new Error(stderr || vscode.l10n.t('Process exited with code {0}', exitCode)); } const result = JSON.parse(stdout.trim()); if (!result.success) { - throw new Error(result.error || 'Unknown expansion error'); + throw new Error(result.error || vscode.l10n.t('Unknown expansion error')); } return result.data; }