diff --git a/README.md b/README.md index db17774..9aa67ea 100644 --- a/README.md +++ b/README.md @@ -65,7 +65,7 @@ Supports [Claude Code](https://docs.anthropic.com/en/docs/claude-code) and [Open { "Cannon07/claude-preview.nvim", config = function() - require("claude-preview").setup() + require("code-preview").setup() end, } ``` @@ -74,7 +74,7 @@ Supports [Claude Code](https://docs.anthropic.com/en/docs/claude-code) and [Open ```lua vim.opt.rtp:prepend("/path/to/claude-preview.nvim") -require("claude-preview").setup() +require("code-preview").setup() ``` --- @@ -85,7 +85,7 @@ require("claude-preview").setup() 1. Install the plugin and call `setup()` 2. Open a project in Neovim -3. Run `:ClaudePreviewInstallHooks` — writes hooks to `.claude/settings.local.json` +3. Run `:CodePreviewInstallClaudeCodeHooks` — writes hooks to `.claude/settings.local.json` 4. Restart Claude Code CLI in the project directory 5. Ask Claude to edit a file — a diff opens automatically in Neovim 6. Accept/reject in the CLI; the diff closes automatically on accept @@ -141,7 +141,7 @@ Both backends communicate with Neovim via RPC (`nvim --server --remote- All options with defaults: ```lua -require("claude-preview").setup({ +require("code-preview").setup({ diff = { layout = "tab", -- "tab" (new tab) | "vsplit" (current tab) | "inline" (GitHub-style) labels = { current = "CURRENT", proposed = "PROPOSED" }, @@ -178,25 +178,28 @@ require("claude-preview").setup({ | Command | Description | |---------|-------------| -| `:ClaudePreviewInstallHooks` | Install Claude Code hooks to `.claude/settings.local.json` | -| `:ClaudePreviewUninstallHooks` | Remove Claude Code hooks (leaves other hooks intact) | +| `:CodePreviewInstallClaudeCodeHooks` | Install Claude Code hooks to `.claude/settings.local.json` | +| `:CodePreviewUninstallClaudeCodeHooks` | Remove Claude Code hooks (leaves other hooks intact) | | `:CodePreviewInstallOpenCodeHooks` | Install OpenCode plugin to `.opencode/plugins/` | | `:CodePreviewUninstallOpenCodeHooks` | Remove OpenCode plugin | -| `:ClaudePreviewCloseDiff` | Manually close the diff (use after rejecting a change) | -| `:ClaudePreviewStatus` | Show socket path, hook status, and dependency check | -| `:checkhealth claude-preview` | Full health check (both backends) | +| `:CodePreviewCloseDiff` | Manually close the diff (use after rejecting a change) | +| `:CodePreviewStatus` | Show socket path, hook status, and dependency check | +| `:CodePreviewToggleVisibleOnly` | Toggle visible_only — show diffs only for open buffers | +| `:checkhealth code-preview` | Full health check (both backends) | + +> **Migrating?** The old `:ClaudePreview*` commands still work but show a deprecation warning. They will be removed in a future release. ## Keymaps | Key | Description | |-----|-------------| -| `dq` | Close the diff (same as `:ClaudePreviewCloseDiff`) | +| `dq` | Close the diff (same as `:CodePreviewCloseDiff`) | --- ## Diff Layouts -claude-preview supports three diff layouts, configured via `diff.layout`: +code-preview supports three diff layouts, configured via `diff.layout`: | Layout | Description | |--------|-------------| @@ -214,7 +217,7 @@ claude-preview supports three diff layouts, configured via `diff.layout`: To use inline diff: ```lua -require("claude-preview").setup({ +require("code-preview").setup({ diff = { layout = "inline" }, }) ``` @@ -223,7 +226,7 @@ require("claude-preview").setup({ ## Neo-tree Integration (Optional) -If you use [neo-tree.nvim](https://github.com/nvim-neo-tree/neo-tree.nvim), claude-preview will automatically decorate your file tree with visual indicators when changes are proposed. No extra configuration is required — it works out of the box. +If you use [neo-tree.nvim](https://github.com/nvim-neo-tree/neo-tree.nvim), code-preview will automatically decorate your file tree with visual indicators when changes are proposed. No extra configuration is required — it works out of the box. ![neo-tree integration demo](docs/claude-preview-neotree-integration.gif) @@ -246,7 +249,7 @@ Additional behaviors: All neo-tree options with defaults: ```lua -require("claude-preview").setup({ +require("code-preview").setup({ neo_tree = { enabled = true, -- set false to disable neo-tree integration position = "right", -- neo-tree window position: "left", "right", "float" @@ -272,24 +275,30 @@ require("claude-preview").setup({ ``` claude-preview.nvim/ -├── lua/claude-preview/ -│ ├── init.lua setup(), config, commands -│ ├── diff.lua show_diff(), close_diff() -│ ├── hooks.lua install/uninstall for both backends -│ ├── changes.lua change status registry (modified/created/deleted) -│ ├── neo_tree.lua neo-tree integration (icons, virtual nodes, reveal) -│ └── health.lua :checkhealth (both backends) -├── bin/ Claude Code hook scripts -│ ├── claude-preview-diff.sh PreToolUse hook entry point -│ ├── claude-close-diff.sh PostToolUse hook entry point -│ ├── nvim-socket.sh Neovim socket discovery -│ ├── nvim-send.sh RPC send helper -│ ├── apply-edit.lua Single Edit transformer -│ └── apply-multi-edit.lua MultiEdit transformer -└── opencode-plugin/ OpenCode plugin - ├── index.ts tool.execute.before/after hooks - ├── nvim.ts Neovim socket discovery + RPC - └── edits.ts Edit computation helpers +├── lua/code-preview/ +│ ├── init.lua setup(), config, commands +│ ├── diff.lua show_diff(), close_diff() +│ ├── changes.lua change status registry (modified/created/deleted) +│ ├── neo_tree.lua neo-tree integration (icons, virtual nodes, reveal) +│ ├── health.lua :checkhealth (both backends) +│ └── backends/ +│ ├── claudecode.lua Claude Code hook install/uninstall +│ └── opencode.lua OpenCode plugin install/uninstall +├── bin/ Shared core scripts +│ ├── core-pre-tool.sh Unified PreToolUse logic +│ ├── core-post-tool.sh Unified PostToolUse logic +│ ├── nvim-socket.sh Neovim socket discovery +│ ├── nvim-send.sh RPC send helper +│ ├── apply-edit.lua Single Edit transformer +│ └── apply-multi-edit.lua MultiEdit transformer +├── backends/ +│ ├── claudecode/ Claude Code adapter +│ │ ├── code-preview-diff.sh PreToolUse hook entry point +│ │ └── code-close-diff.sh PostToolUse hook entry point +│ └── opencode/ OpenCode adapter +│ ├── index.ts tool.execute.before/after hooks +│ ├── package.json +│ └── tsconfig.json ``` --- @@ -299,11 +308,11 @@ claude-preview.nvim/ The test suite uses [plenary.nvim](https://github.com/nvim-lua/plenary.nvim) for core plugin tests and shell scripts for backend integration tests. CI runs on both Ubuntu and macOS. ```bash -./tests/run.sh # all tests (plugin + backends) -./tests/run.sh plugin # core plugin tests only (plenary busted) -./tests/run.sh backends # all backend integration tests -./tests/run.sh backends/claude # Claude Code backend only -./tests/run.sh backends/opencode # OpenCode backend only +./tests/run.sh # all tests (plugin + backends) +./tests/run.sh plugin # core plugin tests only (plenary busted) +./tests/run.sh backends # all backend integration tests +./tests/run.sh backends/claudecode # Claude Code backend only +./tests/run.sh backends/opencode # OpenCode backend only ``` **Dependencies:** Neovim >= 0.10, jq, bun (for OpenCode tests). Plenary is auto-installed to `deps/` on first run. @@ -325,12 +334,12 @@ vim.api.nvim_create_autocmd({ "FocusGained", "BufEnter", "CursorHold" }, { ## Troubleshooting **Diff doesn't open** -- Run `:ClaudePreviewStatus` — check that `Neovim socket` is found -- Run `:checkhealth claude-preview` — check for missing dependencies +- Run `:CodePreviewStatus` — check that `Neovim socket` is found +- Run `:checkhealth code-preview` — check for missing dependencies - Restart the CLI agent after installing hooks (hooks are read at startup) **Claude Code hooks not firing** -- Run `:ClaudePreviewInstallHooks` in the project root +- Run `:CodePreviewInstallClaudeCodeHooks` in the project root - Verify `.claude/settings.local.json` contains the hook entries - Ensure `jq` is in PATH - Restart Claude Code CLI @@ -342,7 +351,12 @@ vim.api.nvim_create_autocmd({ "FocusGained", "BufEnter", "CursorHold" }, { - Restart OpenCode **Diff doesn't close after rejecting** -- Press `dq` or run `:ClaudePreviewCloseDiff` — the post hook only fires on accept +- Press `dq` or run `:CodePreviewCloseDiff` — the post hook only fires on accept + +**Migrating from older versions** +- Update `require("claude-preview")` to `require("code-preview")` in your Neovim config +- Re-run `:CodePreviewInstallClaudeCodeHooks` to update hook paths +- The old `:ClaudePreview*` commands still work but show deprecation warnings --- diff --git a/backends/claudecode/code-close-diff.sh b/backends/claudecode/code-close-diff.sh new file mode 100755 index 0000000..6522ce6 --- /dev/null +++ b/backends/claudecode/code-close-diff.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +# code-close-diff.sh — PostToolUse hook adapter for Claude Code +# Delegates to core-post-tool.sh with the Claude Code backend flag. + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +BIN_DIR="$SCRIPT_DIR/../../bin" +export CODE_PREVIEW_BACKEND="claudecode" +exec "$BIN_DIR/core-post-tool.sh" diff --git a/backends/claudecode/code-preview-diff.sh b/backends/claudecode/code-preview-diff.sh new file mode 100755 index 0000000..99fb0ef --- /dev/null +++ b/backends/claudecode/code-preview-diff.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +# code-preview-diff.sh — PreToolUse hook adapter for Claude Code +# Delegates to core-pre-tool.sh with the Claude Code backend flag. + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +BIN_DIR="$SCRIPT_DIR/../../bin" +export CODE_PREVIEW_BACKEND="claudecode" +exec "$BIN_DIR/core-pre-tool.sh" diff --git a/opencode-plugin/index.ts b/backends/opencode/index.ts similarity index 97% rename from opencode-plugin/index.ts rename to backends/opencode/index.ts index 65d5717..96c3959 100644 --- a/opencode-plugin/index.ts +++ b/backends/opencode/index.ts @@ -21,7 +21,7 @@ function binDir(): string { return readFileSync(resolve(__dirname, "bin-path.txt"), "utf-8").trim() } catch { // Fallback for development: resolve relative to plugin source - return resolve(__dirname, "../bin") + return resolve(__dirname, "../../bin") } } @@ -85,7 +85,7 @@ function runCoreScript(script: string, json: string): void { try { execSync(`"${bin}/${script}"`, { input: json, - env: { ...process.env, CLAUDE_PREVIEW_BACKEND: "opencode" }, + env: { ...process.env, CODE_PREVIEW_BACKEND: "opencode" }, timeout: 15000, stdio: ["pipe", "pipe", "pipe"], }) diff --git a/backends/opencode/package.json b/backends/opencode/package.json new file mode 100644 index 0000000..8d58142 --- /dev/null +++ b/backends/opencode/package.json @@ -0,0 +1,6 @@ +{ + "name": "code-preview-opencode", + "version": "1.0.0", + "description": "OpenCode plugin for code-preview.nvim — sends diff previews to Neovim", + "type": "module" +} diff --git a/opencode-plugin/tsconfig.json b/backends/opencode/tsconfig.json similarity index 100% rename from opencode-plugin/tsconfig.json rename to backends/opencode/tsconfig.json diff --git a/bin/claude-close-diff.sh b/bin/claude-close-diff.sh deleted file mode 100755 index 76dc7c0..0000000 --- a/bin/claude-close-diff.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env bash -# claude-close-diff.sh — PostToolUse hook adapter for Claude Code -# Delegates to core-post-tool.sh with the Claude backend flag. - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -export CLAUDE_PREVIEW_BACKEND="claude" -exec "$SCRIPT_DIR/core-post-tool.sh" diff --git a/bin/claude-preview-diff.sh b/bin/claude-preview-diff.sh deleted file mode 100755 index a5c0c2e..0000000 --- a/bin/claude-preview-diff.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env bash -# claude-preview-diff.sh — PreToolUse hook adapter for Claude Code -# Delegates to core-pre-tool.sh with the Claude backend flag. - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -export CLAUDE_PREVIEW_BACKEND="claude" -exec "$SCRIPT_DIR/core-pre-tool.sh" diff --git a/bin/core-post-tool.sh b/bin/core-post-tool.sh index f1967ad..0dfa002 100755 --- a/bin/core-post-tool.sh +++ b/bin/core-post-tool.sh @@ -9,7 +9,7 @@ # "tool_input": { "file_path": "...", ... } } # # Environment: -# CLAUDE_PREVIEW_BACKEND — "claude" or "opencode" (currently unused, reserved) +# CODE_PREVIEW_BACKEND — "claudecode" or "opencode" (currently unused, reserved) SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" @@ -24,8 +24,8 @@ source "$SCRIPT_DIR/nvim-send.sh" # For Bash tool (rm detection), only clear deletion markers — don't touch edit markers or diff tab if [[ "$TOOL_NAME" == "Bash" ]]; then - nvim_send "require('claude-preview.changes').clear_by_status('deleted')" || true - nvim_send "vim.defer_fn(function() pcall(function() require('claude-preview.neo_tree').refresh() end) end, 200)" || true + nvim_send "require('code-preview.changes').clear_by_status('deleted')" || true + nvim_send "vim.defer_fn(function() pcall(function() require('code-preview.neo_tree').refresh() end) end, 200)" || true exit 0 fi @@ -36,15 +36,15 @@ FILE_PATH_ESC="$(escape_lua "${FILE_PATH:-}")" # Only clean up if a diff for THIS file is actually open. # OpenCode fires all before-hooks before any after-hooks, so the open diff # may belong to a different file — closing it would kill the wrong preview. -DIFF_OPEN=$(nvim --server "$NVIM_SOCKET" --remote-expr "luaeval(\"require('claude-preview.diff').is_open('${FILE_PATH_ESC}')\")" 2>/dev/null || echo "false") +DIFF_OPEN=$(nvim --server "$NVIM_SOCKET" --remote-expr "luaeval(\"require('code-preview.diff').is_open('${FILE_PATH_ESC}')\")" 2>/dev/null || echo "false") if [[ "$DIFF_OPEN" == "true" ]]; then - nvim_send "require('claude-preview.changes').clear_all()" || true - nvim_send "require('claude-preview.diff').close_diff()" || true + nvim_send "require('code-preview.changes').clear_all()" || true + nvim_send "require('code-preview.diff').close_diff()" || true if [[ -n "$FILE_PATH" ]]; then - nvim_send "vim.defer_fn(function() pcall(function() require('claude-preview.neo_tree').refresh() end) vim.defer_fn(function() pcall(function() require('claude-preview.neo_tree').reveal('$FILE_PATH_ESC') end) end, 200) end, 200)" || true + nvim_send "vim.defer_fn(function() pcall(function() require('code-preview.neo_tree').refresh() end) vim.defer_fn(function() pcall(function() require('code-preview.neo_tree').reveal('$FILE_PATH_ESC') end) end, 200) end, 200)" || true else - nvim_send "vim.defer_fn(function() pcall(function() require('claude-preview.neo_tree').refresh() end) end, 200)" || true + nvim_send "vim.defer_fn(function() pcall(function() require('code-preview.neo_tree').refresh() end) end, 200)" || true fi fi diff --git a/bin/core-pre-tool.sh b/bin/core-pre-tool.sh index 0232ac3..d05bc25 100755 --- a/bin/core-pre-tool.sh +++ b/bin/core-pre-tool.sh @@ -10,7 +10,7 @@ # "tool_input": { "file_path": "...", ... } } # # Environment: -# CLAUDE_PREVIEW_BACKEND — "claude" or "opencode" (gates output format) +# CODE_PREVIEW_BACKEND — "claudecode" or "opencode" (gates output format) set -euo pipefail @@ -121,13 +121,13 @@ case "$TOOL_NAME" in if [[ "$HAS_NVIM" == "true" ]]; then for path in $RM_PATHS; do PATH_ESC="$(escape_lua "$path")" - nvim_send "require('claude-preview.changes').set('$PATH_ESC', 'deleted')" || true + nvim_send "require('code-preview.changes').set('$PATH_ESC', 'deleted')" || true done - nvim_send "pcall(function() require('claude-preview.neo_tree').refresh() end)" || true + nvim_send "pcall(function() require('code-preview.neo_tree').refresh() end)" || true # Reveal the first deleted file in the tree FIRST_PATH="$(echo "$RM_PATHS" | awk '{print $1}')" FIRST_ESC="$(escape_lua "$FIRST_PATH")" - nvim_send "vim.defer_fn(function() pcall(function() require('claude-preview.neo_tree').reveal('$FIRST_ESC') end) end, 300)" || true + nvim_send "vim.defer_fn(function() pcall(function() require('code-preview.neo_tree').reveal('$FIRST_ESC') end) end, 300)" || true fi exit 0 ;; @@ -153,7 +153,7 @@ if [[ "$HAS_NVIM" == "true" ]]; then FILE_PATH_ESC="$(escape_lua "$FILE_PATH")" # Query config + file visibility from nvim in a single RPC call - HOOK_CTX=$(nvim --server "$NVIM_SOCKET" --remote-expr "luaeval(\"require('claude-preview').hook_context('${FILE_PATH_ESC}')\")" 2>/dev/null || echo '{}') + HOOK_CTX=$(nvim --server "$NVIM_SOCKET" --remote-expr "luaeval(\"require('code-preview').hook_context('${FILE_PATH_ESC}')\")" 2>/dev/null || echo '{}') # Use explicit conditional: jq's `//` operator treats boolean false like null, # which would silently convert `reveal = false` into `true`. NEO_TREE_REVEAL=$(echo "$HOOK_CTX" | jq -r 'if .neo_tree_reveal == false then "false" else "true" end') @@ -177,7 +177,7 @@ if [[ "$HAS_NVIM" == "true" ]]; then fi if [[ "$SHOULD_SHOW" == "1" ]]; then - nvim_send "require('claude-preview.changes').set('$FILE_PATH_ESC', '$CHANGE_STATUS')" || true + nvim_send "require('code-preview.changes').set('$FILE_PATH_ESC', '$CHANGE_STATUS')" || true # Neo-tree integration (gated by config) if [[ "$NEO_TREE_REVEAL" == "true" ]]; then @@ -204,17 +204,17 @@ if [[ "$HAS_NVIM" == "true" ]]; then fi REVEAL_TARGET_ESC="$(escape_lua "$REVEAL_TARGET")" - nvim_send "pcall(function() require('claude-preview.neo_tree').refresh() end)" || true + nvim_send "pcall(function() require('code-preview.neo_tree').refresh() end)" || true if [[ -n "$REVEAL_DIR" ]]; then REVEAL_DIR_ESC="$(escape_lua "$REVEAL_DIR")" - nvim_send "vim.defer_fn(function() pcall(function() require('claude-preview.neo_tree').reveal('$REVEAL_TARGET_ESC', '$REVEAL_DIR_ESC') end) end, 300)" || true + nvim_send "vim.defer_fn(function() pcall(function() require('code-preview.neo_tree').reveal('$REVEAL_TARGET_ESC', '$REVEAL_DIR_ESC') end) end, 300)" || true else - nvim_send "vim.defer_fn(function() pcall(function() require('claude-preview.neo_tree').reveal('$REVEAL_TARGET_ESC') end) end, 300)" || true + nvim_send "vim.defer_fn(function() pcall(function() require('code-preview.neo_tree').reveal('$REVEAL_TARGET_ESC') end) end, 300)" || true fi fi - nvim_send "require('claude-preview.diff').show_diff('$ORIG_ESC', '$PROP_ESC', '$DISPLAY_ESC', '$FILE_PATH_ESC')" || true + nvim_send "require('code-preview.diff').show_diff('$ORIG_ESC', '$PROP_ESC', '$DISPLAY_ESC', '$FILE_PATH_ESC')" || true fi fi @@ -224,7 +224,7 @@ fi # unreachable), produce no output and let Claude Code's own permission # settings (bypass, ask, allowlist) decide. Otherwise return "ask" to # prompt the user for every edit, preserving the default review workflow. -if [[ "${CLAUDE_PREVIEW_BACKEND:-}" == "claude" && "$HAS_NVIM" == "true" && "$DEFER_PERMISSIONS" != "true" ]]; then +if [[ "${CODE_PREVIEW_BACKEND:-}" == "claudecode" && "$HAS_NVIM" == "true" && "$DEFER_PERMISSIONS" != "true" ]]; then REASON="Diff preview sent to Neovim. Review before accepting." printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"ask","permissionDecisionReason":"%s"}}\n' "$REASON" fi diff --git a/bin/nvim-send.sh b/bin/nvim-send.sh index 29b7043..447d727 100755 --- a/bin/nvim-send.sh +++ b/bin/nvim-send.sh @@ -3,7 +3,7 @@ # # Usage: # source bin/nvim-send.sh -# nvim_send "require('claude-preview.diff').show_diff('a', 'b', 'c')" +# nvim_send "require('code-preview.diff').show_diff('a', 'b', 'c')" # # Depends on nvim-socket.sh being sourced first (NVIM_SOCKET must be set). @@ -24,7 +24,7 @@ nvim_send() { return 1 fi local tmp_lua - tmp_lua="$(mktemp /tmp/claude-preview-nvim-cmd.XXXXXX)" + tmp_lua="$(mktemp /tmp/code-preview-nvim-cmd.XXXXXX)" printf '%s' "$lua_cmd" > "$tmp_lua" nvim --server "$NVIM_SOCKET" --remote-expr "execute('luafile $tmp_lua')" >/dev/null 2>&1 local rc=$? diff --git a/lua/claude-preview/hooks.lua b/lua/claude-preview/hooks.lua deleted file mode 100644 index 6093c10..0000000 --- a/lua/claude-preview/hooks.lua +++ /dev/null @@ -1,189 +0,0 @@ -local M = {} - --- Resolve the absolute path to the plugin's bin/ directory. --- We use debug.getinfo so this works regardless of where the plugin is installed. -local function bin_dir() - local src = debug.getinfo(1, "S").source - -- src is "@/absolute/path/to/lua/claude-preview/hooks.lua" - local lua_file = src:sub(2) -- strip leading "@" - local lua_dir = vim.fn.fnamemodify(lua_file, ":h") -- .../lua/claude-preview - -- Go up two levels: claude-preview/ → lua/ → plugin root, then into bin/ - return vim.fn.fnamemodify(lua_dir, ":h:h") .. "/bin" -end - -local HOOK_MARKER = "claude-preview" -- used to identify our entries - -local function settings_path() - return vim.fn.getcwd() .. "/.claude/settings.local.json" -end - -local function read_settings(path) - local f = io.open(path, "r") - if not f then return {} end - local raw = f:read("*a") - f:close() - if raw == "" then return {} end - local ok, data = pcall(vim.json.decode, raw) - return ok and data or {} -end - -local function write_settings(path, data) - -- Ensure parent directory exists - vim.fn.mkdir(vim.fn.fnamemodify(path, ":h"), "p") - local f = assert(io.open(path, "w"), "Cannot write to " .. path) - f:write(vim.json.encode(data)) - f:close() -end - -function M.install() - local dir = bin_dir() - local preview = dir .. "/claude-preview-diff.sh" - local close = dir .. "/claude-close-diff.sh" - - -- Verify scripts exist - if vim.fn.filereadable(preview) == 0 then - vim.notify("[claude-preview] hook script not found: " .. preview, vim.log.levels.ERROR) - return - end - - local path = settings_path() - local data = read_settings(path) - - -- Initialise missing structure - data.hooks = data.hooks or {} - data.hooks.PreToolUse = data.hooks.PreToolUse or {} - data.hooks.PostToolUse = data.hooks.PostToolUse or {} - - -- Remove any existing claude-preview entries to avoid duplicates - local function remove_ours(list) - local filtered = {} - for _, entry in ipairs(list) do - if not (entry.hooks and entry.hooks[1] and - tostring(entry.hooks[1].command or ""):find(HOOK_MARKER, 1, true)) then - table.insert(filtered, entry) - end - end - return filtered - end - - data.hooks.PreToolUse = remove_ours(data.hooks.PreToolUse) - data.hooks.PostToolUse = remove_ours(data.hooks.PostToolUse) - - -- Add our entries - table.insert(data.hooks.PreToolUse, { - matcher = "Edit|Write|MultiEdit|Bash", - hooks = { { type = "command", command = preview } }, - }) - table.insert(data.hooks.PostToolUse, { - matcher = "Edit|Write|MultiEdit|Bash", - hooks = { { type = "command", command = close } }, - }) - - write_settings(path, data) - vim.notify("[claude-preview] Hooks installed → " .. path, vim.log.levels.INFO) -end - -function M.uninstall() - local path = settings_path() - local data = read_settings(path) - - if not data.hooks then - vim.notify("[claude-preview] No hooks found in " .. path, vim.log.levels.WARN) - return - end - - local function remove_ours(list) - local filtered = {} - for _, entry in ipairs(list or {}) do - if not (entry.hooks and entry.hooks[1] and - tostring(entry.hooks[1].command or ""):find(HOOK_MARKER, 1, true)) then - table.insert(filtered, entry) - end - end - return filtered - end - - data.hooks.PreToolUse = remove_ours(data.hooks.PreToolUse) - data.hooks.PostToolUse = remove_ours(data.hooks.PostToolUse) - - write_settings(path, data) - vim.notify("[claude-preview] Hooks removed from " .. path, vim.log.levels.INFO) -end - --- ── OpenCode plugin management ────────────────────────────────── - -local function plugin_source_dir() - local src = debug.getinfo(1, "S").source - local lua_file = src:sub(2) - local lua_dir = vim.fn.fnamemodify(lua_file, ":h") - return vim.fn.fnamemodify(lua_dir, ":h:h") .. "/opencode-plugin" -end - -local function opencode_target_dir() - return vim.fn.getcwd() .. "/.opencode/plugins" -end - -function M.install_opencode() - local source = plugin_source_dir() - local index_src = source .. "/index.ts" - - if vim.fn.filereadable(index_src) == 0 then - vim.notify("[claude-preview] OpenCode plugin source not found: " .. index_src, vim.log.levels.ERROR) - return - end - - local target = opencode_target_dir() - vim.fn.mkdir(target, "p") - - -- Copy plugin files (nvim.ts and edits.ts removed — core scripts handle this now) - local files = { "index.ts", "package.json", "tsconfig.json" } - for _, file in ipairs(files) do - local src_path = source .. "/" .. file - local dst_path = target .. "/" .. file - if vim.fn.filereadable(src_path) == 1 then - vim.fn.system({ "cp", src_path, dst_path }) - end - end - - -- Write bin-path.txt so the plugin can find the core scripts - local bin_path_file = target .. "/bin-path.txt" - local bf = io.open(bin_path_file, "w") - if bf then - bf:write(bin_dir()) - bf:close() - end - - vim.notify("[claude-preview] OpenCode plugin installed → " .. target, vim.log.levels.INFO) -end - -function M.uninstall_opencode() - local target = opencode_target_dir() - - local files = { "index.ts", "package.json", "tsconfig.json", "bin-path.txt" } - local removed = false - for _, file in ipairs(files) do - local path = target .. "/" .. file - if vim.fn.filereadable(path) == 1 then - vim.fn.delete(path) - removed = true - end - end - - -- Also clean up legacy files from previous versions - for _, legacy in ipairs({ "nvim.ts", "edits.ts" }) do - local path = target .. "/" .. legacy - if vim.fn.filereadable(path) == 1 then - vim.fn.delete(path) - end - end - - if removed then - -- Remove plugins/ directory only if empty (don't touch other .opencode files) - vim.fn.delete(target, "d") - vim.notify("[claude-preview] OpenCode plugin removed", vim.log.levels.INFO) - else - vim.notify("[claude-preview] No OpenCode plugin found in " .. target, vim.log.levels.WARN) - end -end - -return M diff --git a/lua/code-preview/backends/claudecode.lua b/lua/code-preview/backends/claudecode.lua new file mode 100644 index 0000000..a814810 --- /dev/null +++ b/lua/code-preview/backends/claudecode.lua @@ -0,0 +1,118 @@ +local M = {} + +-- Resolve the absolute path to the plugin's bin/ directory. +-- We use debug.getinfo so this works regardless of where the plugin is installed. +-- Resolve plugin root from this file's location +local function plugin_root() + local src = debug.getinfo(1, "S").source + -- src is "@/absolute/path/to/lua/code-preview/backends/claudecode.lua" + local lua_file = src:sub(2) -- strip leading "@" + local lua_dir = vim.fn.fnamemodify(lua_file, ":h") -- .../lua/code-preview/backends + -- Go up three levels: backends/ → code-preview/ → lua/ → plugin root + return vim.fn.fnamemodify(lua_dir, ":h:h:h") +end + +-- Path to shared utilities (bin/) +local function bin_dir() + return plugin_root() .. "/bin" +end + +-- Path to Claude Code adapter scripts (backends/claudecode/) +local function scripts_dir() + return plugin_root() .. "/backends/claudecode" +end + +local HOOK_MARKER = "code-preview" +local LEGACY_HOOK_MARKER = "claude-preview" -- match old entries during transition + +local function settings_path() + return vim.fn.getcwd() .. "/.claude/settings.local.json" +end + +local function read_settings(path) + local f = io.open(path, "r") + if not f then return {} end + local raw = f:read("*a") + f:close() + if raw == "" then return {} end + local ok, data = pcall(vim.json.decode, raw) + return ok and data or {} +end + +local function write_settings(path, data) + -- Ensure parent directory exists + vim.fn.mkdir(vim.fn.fnamemodify(path, ":h"), "p") + local f = assert(io.open(path, "w"), "Cannot write to " .. path) + f:write(vim.json.encode(data)) + f:close() +end + +-- Remove entries matching either the current or legacy hook marker. +-- This lets users who installed with the old name uninstall after upgrading. +local function remove_ours(list) + local filtered = {} + for _, entry in ipairs(list) do + local cmd = "" + if entry.hooks and entry.hooks[1] then + cmd = tostring(entry.hooks[1].command or "") + end + if not (cmd:find(HOOK_MARKER, 1, true) or cmd:find(LEGACY_HOOK_MARKER, 1, true)) then + table.insert(filtered, entry) + end + end + return filtered +end + +function M.install() + local dir = scripts_dir() + local preview = dir .. "/code-preview-diff.sh" + local close = dir .. "/code-close-diff.sh" + + -- Verify scripts exist + if vim.fn.filereadable(preview) == 0 then + vim.notify("[code-preview] hook script not found: " .. preview, vim.log.levels.ERROR) + return + end + + local path = settings_path() + local data = read_settings(path) + + -- Initialise missing structure + data.hooks = data.hooks or {} + data.hooks.PreToolUse = data.hooks.PreToolUse or {} + data.hooks.PostToolUse = data.hooks.PostToolUse or {} + + data.hooks.PreToolUse = remove_ours(data.hooks.PreToolUse) + data.hooks.PostToolUse = remove_ours(data.hooks.PostToolUse) + + -- Add our entries + table.insert(data.hooks.PreToolUse, { + matcher = "Edit|Write|MultiEdit|Bash", + hooks = { { type = "command", command = preview } }, + }) + table.insert(data.hooks.PostToolUse, { + matcher = "Edit|Write|MultiEdit|Bash", + hooks = { { type = "command", command = close } }, + }) + + write_settings(path, data) + vim.notify("[code-preview] Hooks installed → " .. path, vim.log.levels.INFO) +end + +function M.uninstall() + local path = settings_path() + local data = read_settings(path) + + if not data.hooks then + vim.notify("[code-preview] No hooks found in " .. path, vim.log.levels.WARN) + return + end + + data.hooks.PreToolUse = remove_ours(data.hooks.PreToolUse or {}) + data.hooks.PostToolUse = remove_ours(data.hooks.PostToolUse or {}) + + write_settings(path, data) + vim.notify("[code-preview] Hooks removed from " .. path, vim.log.levels.INFO) +end + +return M diff --git a/lua/code-preview/backends/opencode.lua b/lua/code-preview/backends/opencode.lua new file mode 100644 index 0000000..4e27a18 --- /dev/null +++ b/lua/code-preview/backends/opencode.lua @@ -0,0 +1,88 @@ +local M = {} + +-- Resolve plugin root from this file's location +local function plugin_root() + local src = debug.getinfo(1, "S").source + -- src is "@/absolute/path/to/lua/code-preview/backends/opencode.lua" + local lua_file = src:sub(2) + local lua_dir = vim.fn.fnamemodify(lua_file, ":h") -- .../lua/code-preview/backends + -- Go up three levels: backends/ → code-preview/ → lua/ → plugin root + return vim.fn.fnamemodify(lua_dir, ":h:h:h") +end + +local function bin_dir() + return plugin_root() .. "/bin" +end + +local function plugin_source_dir() + return plugin_root() .. "/backends/opencode" +end + +local function opencode_target_dir() + return vim.fn.getcwd() .. "/.opencode/plugins" +end + +function M.install() + local source = plugin_source_dir() + local index_src = source .. "/index.ts" + + if vim.fn.filereadable(index_src) == 0 then + vim.notify("[code-preview] OpenCode plugin source not found: " .. index_src, vim.log.levels.ERROR) + return + end + + local target = opencode_target_dir() + vim.fn.mkdir(target, "p") + + -- Copy plugin files + local files = { "index.ts", "package.json", "tsconfig.json" } + for _, file in ipairs(files) do + local src_path = source .. "/" .. file + local dst_path = target .. "/" .. file + if vim.fn.filereadable(src_path) == 1 then + vim.fn.system({ "cp", src_path, dst_path }) + end + end + + -- Write bin-path.txt so the plugin can find the core scripts + local bin_path_file = target .. "/bin-path.txt" + local bf = io.open(bin_path_file, "w") + if bf then + bf:write(bin_dir()) + bf:close() + end + + vim.notify("[code-preview] OpenCode plugin installed → " .. target, vim.log.levels.INFO) +end + +function M.uninstall() + local target = opencode_target_dir() + + local files = { "index.ts", "package.json", "tsconfig.json", "bin-path.txt" } + local removed = false + for _, file in ipairs(files) do + local path = target .. "/" .. file + if vim.fn.filereadable(path) == 1 then + vim.fn.delete(path) + removed = true + end + end + + -- Also clean up legacy files from previous versions + for _, legacy in ipairs({ "nvim.ts", "edits.ts" }) do + local path = target .. "/" .. legacy + if vim.fn.filereadable(path) == 1 then + vim.fn.delete(path) + end + end + + if removed then + -- Remove plugins/ directory only if empty (don't touch other .opencode files) + vim.fn.delete(target, "d") + vim.notify("[code-preview] OpenCode plugin removed", vim.log.levels.INFO) + else + vim.notify("[code-preview] No OpenCode plugin found in " .. target, vim.log.levels.WARN) + end +end + +return M diff --git a/lua/claude-preview/changes.lua b/lua/code-preview/changes.lua similarity index 100% rename from lua/claude-preview/changes.lua rename to lua/code-preview/changes.lua diff --git a/lua/claude-preview/diff.lua b/lua/code-preview/diff.lua similarity index 97% rename from lua/claude-preview/diff.lua rename to lua/code-preview/diff.lua index cd1f39b..56aa189 100644 --- a/lua/claude-preview/diff.lua +++ b/lua/code-preview/diff.lua @@ -294,7 +294,7 @@ local function show_inline_diff(original_path, proposed_path, real_file_path, cf vim.wo[win].wrap = false vim.wo[win].cursorline = true vim.wo[win].signcolumn = "no" - vim.wo[win].statuscolumn = "%!v:lua.require('claude-preview.diff').inline_statuscolumn(" .. col_width .. ")" + vim.wo[win].statuscolumn = "%!v:lua.require('code-preview.diff').inline_statuscolumn(" .. col_width .. ")" diff_bufs = { buf } @@ -362,7 +362,7 @@ function M.show_diff(original_path, proposed_path, real_file_path, abs_file_path -- real_file_path is the display name shown in the winbar. diff_file_path = abs_file_path or real_file_path - local cfg = require("claude-preview").config + local cfg = require("code-preview").config -- Inline layout: single-buffer unified diff if cfg.diff.layout == "inline" then @@ -434,7 +434,7 @@ function M.show_diff(original_path, proposed_path, real_file_path, abs_file_path end -- Re-equalize when terminal is resized (e.g. tmux pane zoom/unzoom) - diff_augroup = vim.api.nvim_create_augroup("ClaudePreviewDiffResize", { clear = true }) + diff_augroup = vim.api.nvim_create_augroup("CodePreviewDiffResize", { clear = true }) vim.api.nvim_create_autocmd("VimResized", { group = diff_augroup, callback = function() @@ -513,8 +513,8 @@ end function M.close_diff_and_clear() diff_queue = {} -- discard pending diffs on manual close M.close_diff() - pcall(function() require("claude-preview.changes").clear_all() end) - pcall(function() require("claude-preview.neo_tree").refresh() end) + pcall(function() require("code-preview.changes").clear_all() end) + pcall(function() require("code-preview.neo_tree").refresh() end) end return M diff --git a/lua/claude-preview/health.lua b/lua/code-preview/health.lua similarity index 67% rename from lua/claude-preview/health.lua rename to lua/code-preview/health.lua index 8eb3d0d..d2696b5 100644 --- a/lua/claude-preview/health.lua +++ b/lua/code-preview/health.lua @@ -10,7 +10,7 @@ function M.check() -- ── Common ──────────────────────────────────────────────────── - start("claude-preview.nvim") + start("code-preview.nvim") -- Neovim RPC socket (required for both backends) local socket = vim.v.servername or "" @@ -21,7 +21,7 @@ function M.check() end -- Diff layout - local cfg = require("claude-preview").config or {} + local cfg = require("code-preview").config or {} local layout = (cfg.diff and cfg.diff.layout) or "unknown" ok("Diff layout: " .. layout) @@ -40,11 +40,27 @@ function M.check() local src = debug.getinfo(1, "S").source local lua_file = src:sub(2) local lua_dir = vim.fn.fnamemodify(lua_file, ":h") - local bin = vim.fn.fnamemodify(lua_dir, ":h:h") .. "/bin" + local plugin_root = vim.fn.fnamemodify(lua_dir, ":h:h") + local bin = plugin_root .. "/bin" + local claudecode_dir = plugin_root .. "/backends/claudecode" + -- Claude Code adapter scripts + for _, script in ipairs({ + "code-preview-diff.sh", + "code-close-diff.sh", + }) do + local path = claudecode_dir .. "/" .. script + if vim.fn.filereadable(path) == 1 and vim.fn.executable(path) == 1 then + ok(script .. " is executable") + elseif vim.fn.filereadable(path) == 1 then + warn(script .. " exists but is not executable (run: chmod +x " .. path .. ")") + else + error(script .. " not found at " .. path) + end + end + + -- Shared scripts for _, script in ipairs({ - "claude-preview-diff.sh", - "claude-close-diff.sh", "nvim-socket.sh", "nvim-send.sh", "apply-edit.lua", @@ -64,7 +80,7 @@ function M.check() local settings = vim.fn.getcwd() .. "/.claude/settings.local.json" local f = io.open(settings, "r") if not f then - warn(".claude/settings.local.json not found — run :ClaudePreviewInstallHooks") + warn(".claude/settings.local.json not found — run :CodePreviewInstallClaudeCodeHooks") else local raw = f:read("*a") f:close() @@ -72,20 +88,28 @@ function M.check() if not parsed_ok then error(".claude/settings.local.json is invalid JSON") elseif not (data.hooks and data.hooks.PreToolUse) then - warn(".claude/settings.local.json exists but claude-preview hooks are not installed") + warn(".claude/settings.local.json exists but code-preview hooks are not installed") else - local found = false + local found_new = false + local found_legacy = false for _, entry in ipairs(data.hooks.PreToolUse) do - if entry.hooks and entry.hooks[1] and - tostring(entry.hooks[1].command or ""):find("claude-preview", 1, true) then - found = true + local cmd = "" + if entry.hooks and entry.hooks[1] then + cmd = tostring(entry.hooks[1].command or "") + end + if cmd:find("code-preview", 1, true) then + found_new = true break + elseif cmd:find("claude-preview", 1, true) then + found_legacy = true end end - if found then + if found_new then ok("Claude Code hooks are installed") + elseif found_legacy then + warn("Legacy claude-preview hooks detected — run :CodePreviewInstallClaudeCodeHooks to update") else - warn("claude-preview hooks not found — run :ClaudePreviewInstallHooks") + warn("code-preview hooks not found — run :CodePreviewInstallClaudeCodeHooks") end end end diff --git a/lua/claude-preview/init.lua b/lua/code-preview/init.lua similarity index 65% rename from lua/claude-preview/init.lua rename to lua/code-preview/init.lua index 690c799..f0fc026 100644 --- a/lua/claude-preview/init.lua +++ b/lua/code-preview/init.lua @@ -67,50 +67,71 @@ local function deep_merge(base, override) return result end +-- Helper: create a deprecated alias that warns and delegates +local function deprecated_alias(old_name, new_name) + vim.api.nvim_create_user_command(old_name, function() + vim.notify( + "[code-preview] :" .. old_name .. " is deprecated, use :" .. new_name, + vim.log.levels.WARN + ) + vim.cmd(new_name) + end, { desc = "(deprecated) Use :" .. new_name .. " instead" }) +end + function M.setup(user_config) M.config = deep_merge(default_config, user_config or {}) - vim.api.nvim_create_user_command("ClaudePreviewInstallHooks", function() - require("claude-preview.hooks").install() - end, { desc = "Install claude-preview PreToolUse/PostToolUse hooks" }) + -- ── New commands ────────────────────────────────────────────── - vim.api.nvim_create_user_command("ClaudePreviewUninstallHooks", function() - require("claude-preview.hooks").uninstall() - end, { desc = "Uninstall claude-preview hooks" }) + vim.api.nvim_create_user_command("CodePreviewInstallClaudeCodeHooks", function() + require("code-preview.backends.claudecode").install() + end, { desc = "Install code-preview PreToolUse/PostToolUse hooks for Claude Code" }) + + vim.api.nvim_create_user_command("CodePreviewUninstallClaudeCodeHooks", function() + require("code-preview.backends.claudecode").uninstall() + end, { desc = "Uninstall code-preview hooks for Claude Code" }) vim.api.nvim_create_user_command("CodePreviewInstallOpenCodeHooks", function() - require("claude-preview.hooks").install_opencode() - end, { desc = "Install claude-preview plugin for OpenCode" }) + require("code-preview.backends.opencode").install() + end, { desc = "Install code-preview plugin for OpenCode" }) vim.api.nvim_create_user_command("CodePreviewUninstallOpenCodeHooks", function() - require("claude-preview.hooks").uninstall_opencode() - end, { desc = "Uninstall claude-preview plugin from OpenCode" }) + require("code-preview.backends.opencode").uninstall() + end, { desc = "Uninstall code-preview plugin from OpenCode" }) - vim.api.nvim_create_user_command("ClaudePreviewCloseDiff", function() - require("claude-preview.diff").close_diff_and_clear() - end, { desc = "Manually close claude-preview diff (use after rejecting a change)" }) + vim.api.nvim_create_user_command("CodePreviewCloseDiff", function() + require("code-preview.diff").close_diff_and_clear() + end, { desc = "Manually close code-preview diff (use after rejecting a change)" }) - vim.api.nvim_create_user_command("ClaudePreviewStatus", function() + vim.api.nvim_create_user_command("CodePreviewStatus", function() M.status() - end, { desc = "Show claude-preview status" }) + end, { desc = "Show code-preview status" }) - vim.api.nvim_create_user_command("ClaudePreviewToggleVisibleOnly", function() + vim.api.nvim_create_user_command("CodePreviewToggleVisibleOnly", function() M.config.diff.visible_only = not M.config.diff.visible_only vim.notify( - "claude-preview: visible_only = " .. tostring(M.config.diff.visible_only), + "code-preview: visible_only = " .. tostring(M.config.diff.visible_only), vim.log.levels.INFO, - { title = "claude-preview" } + { title = "code-preview" } ) end, { desc = "Toggle visible_only — show diffs only for open buffers vs all files" }) + -- ── Deprecated aliases (remove after one release cycle) ─────── + + deprecated_alias("ClaudePreviewInstallHooks", "CodePreviewInstallClaudeCodeHooks") + deprecated_alias("ClaudePreviewUninstallHooks", "CodePreviewUninstallClaudeCodeHooks") + deprecated_alias("ClaudePreviewCloseDiff", "CodePreviewCloseDiff") + deprecated_alias("ClaudePreviewStatus", "CodePreviewStatus") + deprecated_alias("ClaudePreviewToggleVisibleOnly", "CodePreviewToggleVisibleOnly") + -- Neo-tree integration (soft dependency) if M.config.neo_tree.enabled then - require("claude-preview.neo_tree").setup(M.config) + require("code-preview.neo_tree").setup(M.config) end vim.keymap.set("n", "dq", function() - require("claude-preview.diff").close_diff_and_clear() - end, { desc = "Close claude-preview diff" }) + require("code-preview.diff").close_diff_and_clear() + end, { desc = "Close code-preview diff" }) end --- Query hook context for the PreToolUse shell script. @@ -152,7 +173,7 @@ function M.hook_context(file_path) end function M.status() - local lines = { "claude-preview.nvim status", string.rep("─", 40) } + local lines = { "code-preview.nvim status", string.rep("─", 40) } -- Socket local socket = vim.env.NVIM_LISTEN_ADDRESS or "" @@ -172,7 +193,9 @@ function M.status() if f then local content = f:read("*a") f:close() - hooks_ok = content:find("claude-preview-diff", 1, true) ~= nil + -- Detect both new and legacy hook markers + hooks_ok = content:find("code-preview", 1, true) ~= nil + or content:find("claude-preview", 1, true) ~= nil end table.insert(lines, "Hooks : " .. (hooks_ok and "installed" or "not installed")) @@ -181,10 +204,10 @@ function M.status() table.insert(lines, "jq : " .. (jq_ok and "found" or "MISSING")) -- Diff tab open? - local diff = require("claude-preview.diff") + local diff = require("code-preview.diff") table.insert(lines, "Diff tab : " .. (diff.is_open() and "open" or "closed")) - vim.notify(table.concat(lines, "\n"), vim.log.levels.INFO, { title = "claude-preview" }) + vim.notify(table.concat(lines, "\n"), vim.log.levels.INFO, { title = "code-preview" }) end return M diff --git a/lua/claude-preview/neo_tree.lua b/lua/code-preview/neo_tree.lua similarity index 93% rename from lua/claude-preview/neo_tree.lua rename to lua/code-preview/neo_tree.lua index 41fe477..9374509 100644 --- a/lua/claude-preview/neo_tree.lua +++ b/lua/code-preview/neo_tree.lua @@ -1,6 +1,6 @@ local M = {} -local changes = require("claude-preview.changes") +local changes = require("code-preview.changes") -- Guard: all neo-tree interaction goes through pcall local has_neo_tree = false @@ -72,11 +72,11 @@ local function wrap_name_component(state) local lookup = s.claude_status_lookup or {} local status = resolve_status(lookup, node.path) if node._claude_virtual or status == "created" then - result.highlight = "ClaudePreviewTreeVirtual" + result.highlight = "CodePreviewTreeVirtual" elseif status == "modified" then - result.highlight = "ClaudePreviewTreeModified" + result.highlight = "CodePreviewTreeModified" elseif status == "deleted" then - result.highlight = "ClaudePreviewTreeDeleted" + result.highlight = "CodePreviewTreeDeleted" end end return result @@ -125,17 +125,17 @@ local function inject_status_component(state, symbols) if node._claude_virtual or status == "created" then return { text = (symbols.created or "") .. " ", - highlight = "ClaudePreviewTreeCreated", + highlight = "CodePreviewTreeCreated", } elseif status == "modified" then return { text = (symbols.modified or "󰏫") .. " ", - highlight = "ClaudePreviewTreeModified", + highlight = "CodePreviewTreeModified", } elseif status == "deleted" then return { text = (symbols.deleted or "󰆴") .. " ", - highlight = "ClaudePreviewTreeDeleted", + highlight = "CodePreviewTreeDeleted", } end return {} @@ -285,10 +285,10 @@ function M.setup(cfg) local highlights = cfg.neo_tree.highlights -- Define highlight groups from config - define_hl("ClaudePreviewTreeModified", highlights.modified) - define_hl("ClaudePreviewTreeCreated", highlights.created) - define_hl("ClaudePreviewTreeDeleted", highlights.deleted) - define_hl("ClaudePreviewTreeVirtual", highlights.created, { italic = true }) + define_hl("CodePreviewTreeModified", highlights.modified) + define_hl("CodePreviewTreeCreated", highlights.created) + define_hl("CodePreviewTreeDeleted", highlights.deleted) + define_hl("CodePreviewTreeVirtual", highlights.created, { italic = true }) -- Subscribe to BEFORE_RENDER to inject our lookup and components neo_tree_events.subscribe({ @@ -350,7 +350,7 @@ function M.reveal(filepath, dir) return end pcall(function() - local cfg = require("claude-preview").config + local cfg = require("code-preview").config local position = cfg.neo_tree.position or "right" local opts = { action = "show", diff --git a/opencode-plugin/package.json b/opencode-plugin/package.json deleted file mode 100644 index dd39b2e..0000000 --- a/opencode-plugin/package.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "claude-preview-opencode", - "version": "1.0.0", - "description": "OpenCode plugin for claude-preview.nvim — sends diff previews to Neovim", - "type": "module" -} diff --git a/tests/backends/claude/test_edit.sh b/tests/backends/claudecode/test_edit.sh similarity index 74% rename from tests/backends/claude/test_edit.sh rename to tests/backends/claudecode/test_edit.sh index 593d04e..bb41eaf 100644 --- a/tests/backends/claude/test_edit.sh +++ b/tests/backends/claudecode/test_edit.sh @@ -2,8 +2,8 @@ # test_edit.sh — E2E tests for Claude Code Edit/Write/MultiEdit workflows # # Tests the full pipeline: -# JSON payload → claude-preview-diff.sh → RPC → Neovim state -# JSON payload → claude-close-diff.sh → RPC → cleanup +# JSON payload → code-preview-diff.sh → RPC → Neovim state +# JSON payload → code-close-diff.sh → RPC → cleanup # ── Setup ──────────────────────────────────────────────────────── @@ -44,12 +44,12 @@ EOF # Diff tab should be open local is_open - is_open="$(nvim_eval "require('claude-preview.diff').is_open()")" + is_open="$(nvim_eval "require('code-preview.diff').is_open()")" assert_eq "true" "$is_open" "diff should be open after Edit PreToolUse" || return 1 # Changes registry should have the file marked as modified local change_status - change_status="$(nvim_eval "require('claude-preview.changes').get('$test_file')")" + change_status="$(nvim_eval "require('code-preview.changes').get('$test_file')")" assert_eq "modified" "$change_status" "file should be marked as modified" || return 1 # Proposed temp file should contain the edit result @@ -64,12 +64,12 @@ EOF sleep 0.5 local is_open_after - is_open_after="$(nvim_eval "require('claude-preview.diff').is_open()")" + is_open_after="$(nvim_eval "require('code-preview.diff').is_open()")" assert_eq "false" "$is_open_after" "diff should be closed after PostToolUse" || return 1 # Changes should be cleared local changes_after - changes_after="$(nvim_eval "vim.tbl_count(require('claude-preview.changes').get_all())")" + changes_after="$(nvim_eval "vim.tbl_count(require('code-preview.changes').get_all())")" assert_eq "0" "$changes_after" "changes should be cleared after PostToolUse" || return 1 } @@ -100,12 +100,12 @@ EOF sleep 0.5 local is_open - is_open="$(nvim_eval "require('claude-preview.diff').is_open()")" + is_open="$(nvim_eval "require('code-preview.diff').is_open()")" assert_eq "true" "$is_open" "diff should be open for Write tool" || return 1 # New file should be marked as "created" (file doesn't exist on disk) local change_status - change_status="$(nvim_eval "require('claude-preview.changes').get('$new_file')")" + change_status="$(nvim_eval "require('code-preview.changes').get('$new_file')")" assert_eq "created" "$change_status" "new file should be marked as created" || return 1 # Close @@ -113,7 +113,7 @@ EOF sleep 0.5 local is_open_after - is_open_after="$(nvim_eval "require('claude-preview.diff').is_open()")" + is_open_after="$(nvim_eval "require('code-preview.diff').is_open()")" assert_eq "false" "$is_open_after" "diff should be closed" || return 1 } @@ -145,7 +145,7 @@ EOF sleep 0.5 local change_status - change_status="$(nvim_eval "require('claude-preview.changes').get('$test_file')")" + change_status="$(nvim_eval "require('code-preview.changes').get('$test_file')")" assert_eq "modified" "$change_status" "existing file should be marked as modified" || return 1 run_posttool_hook "$payload" >/dev/null @@ -177,7 +177,7 @@ EOF # File should be marked as deleted in changes registry local change_status - change_status="$(nvim_eval "require('claude-preview.changes').get('$test_file')")" + change_status="$(nvim_eval "require('code-preview.changes').get('$test_file')")" assert_eq "deleted" "$change_status" "rm target should be marked as deleted" || return 1 # PostToolUse for Bash should clear deletion markers only @@ -185,7 +185,7 @@ EOF sleep 0.5 local change_after - change_after="$(nvim_eval "require('claude-preview.changes').get('$test_file') or 'nil'")" + change_after="$(nvim_eval "require('code-preview.changes').get('$test_file') or 'nil'")" assert_eq "nil" "$change_after" "deletion marker should be cleared after PostToolUse" || return 1 } @@ -210,7 +210,7 @@ EOF sleep 0.3 local changes_count - changes_count="$(nvim_eval "vim.tbl_count(require('claude-preview.changes').get_all())")" + changes_count="$(nvim_eval "vim.tbl_count(require('code-preview.changes').get_all())")" assert_eq "0" "$changes_count" "non-rm bash should not set any changes" || return 1 } @@ -298,7 +298,7 @@ EOF run_pretool_hook "$payload1" >/dev/null sleep 0.5 local is_open1 - is_open1="$(nvim_eval "require('claude-preview.diff').is_open()")" + is_open1="$(nvim_eval "require('code-preview.diff').is_open()")" assert_eq "true" "$is_open1" "first diff should be open" || return 1 run_posttool_hook "$payload1" >/dev/null @@ -322,17 +322,90 @@ EOF run_pretool_hook "$payload2" >/dev/null sleep 0.5 local is_open2 - is_open2="$(nvim_eval "require('claude-preview.diff').is_open()")" + is_open2="$(nvim_eval "require('code-preview.diff').is_open()")" assert_eq "true" "$is_open2" "second diff should be open" || return 1 run_posttool_hook "$payload2" >/dev/null sleep 0.5 local is_open_final - is_open_final="$(nvim_eval "require('claude-preview.diff').is_open()")" + is_open_final="$(nvim_eval "require('code-preview.diff').is_open()")" assert_eq "false" "$is_open_final" "diff should be closed after both edits" || return 1 } +# ── Test: visible_only skips diff for non-open files ───────────── + +test_visible_only() { + reset_test_state + + # Enable visible_only in the running Neovim + nvim_exec "require('code-preview').config.diff.visible_only = true" + + local visible_file hidden_file + visible_file="$(create_test_file "src/visible.lua" 'local v = 1')" + hidden_file="$(create_test_file "src/hidden.lua" 'local h = 2')" + + # Open visible_file in a Neovim window so hook_context() reports file_visible=true + nvim_exec "vim.cmd('edit $visible_file')" + sleep 0.2 + + # Pre-tool for the visible file — diff SHOULD open + local payload1 + payload1=$(cat </dev/null + sleep 0.5 + + local is_open + is_open="$(nvim_eval "require('code-preview.diff').is_open()")" + if ! assert_eq "true" "$is_open" "diff should open for a buffer that is visible"; then + nvim_exec "require('code-preview').config.diff.visible_only = false" + return 1 + fi + + run_posttool_hook "$payload1" >/dev/null + sleep 0.3 + + # Pre-tool for the hidden file — diff should NOT open + local payload2 + payload2=$(cat </dev/null + sleep 0.5 + + local is_open2 + is_open2="$(nvim_eval "require('code-preview.diff').is_open()")" + + # Restore config before asserting so teardown is unaffected + nvim_exec "require('code-preview').config.diff.visible_only = false" + + assert_eq "false" "$is_open2" "diff should be skipped for a file not open in any window" || return 1 +} + # ── Run all tests ──────────────────────────────────────────────── run_test "Edit tool opens diff and PostToolUse closes it" test_edit_opens_diff @@ -343,6 +416,7 @@ run_test "Non-rm Bash command is ignored" test_bash_non_rm_passthrough run_test "Unknown tool is silently ignored" test_unknown_tool_passthrough run_test "Edit with replace_all replaces all occurrences" test_edit_replace_all run_test "Sequential edits open/close diff correctly" test_sequential_edits +run_test "visible_only skips diff for files not open in Neovim" test_visible_only # ── Teardown ───────────────────────────────────────────────────── diff --git a/tests/backends/claude/test_install.sh b/tests/backends/claudecode/test_install.sh similarity index 77% rename from tests/backends/claude/test_install.sh rename to tests/backends/claudecode/test_install.sh index beed744..34065a7 100644 --- a/tests/backends/claude/test_install.sh +++ b/tests/backends/claudecode/test_install.sh @@ -6,13 +6,13 @@ setup_test_project start_nvim -# Change Neovim's cwd to the test project so hooks.lua writes settings there +# Change Neovim's cwd to the test project so backend module writes settings there nvim_exec "vim.cmd('cd $TEST_PROJECT_DIR')" # ── Test: Install Claude Code hooks ────────────────────────────── test_install_claude_hooks() { - nvim_exec "require('claude-preview.hooks').install()" + nvim_exec "require('code-preview.backends.claudecode').install()" sleep 0.3 local settings_file="$TEST_PROJECT_DIR/.claude/settings.local.json" @@ -24,8 +24,8 @@ test_install_claude_hooks() { # Should have PreToolUse and PostToolUse hooks assert_contains "$content" "PreToolUse" "should have PreToolUse hook" || return 1 assert_contains "$content" "PostToolUse" "should have PostToolUse hook" || return 1 - assert_contains "$content" "claude-preview-diff.sh" "should reference diff script" || return 1 - assert_contains "$content" "claude-close-diff.sh" "should reference close script" || return 1 + assert_contains "$content" "code-preview-diff.sh" "should reference diff script" || return 1 + assert_contains "$content" "code-close-diff.sh" "should reference close script" || return 1 assert_contains "$content" "Edit|Write|MultiEdit|Bash" "should match Edit/Write/MultiEdit/Bash tools" || return 1 } @@ -33,11 +33,11 @@ test_install_claude_hooks() { test_uninstall_claude_hooks() { # Install first - nvim_exec "require('claude-preview.hooks').install()" + nvim_exec "require('code-preview.backends.claudecode').install()" sleep 0.2 # Then uninstall - nvim_exec "require('claude-preview.hooks').uninstall()" + nvim_exec "require('code-preview.backends.claudecode').uninstall()" sleep 0.2 local settings_file="$TEST_PROJECT_DIR/.claude/settings.local.json" @@ -47,15 +47,15 @@ test_uninstall_claude_hooks() { content="$(cat "$settings_file")" # Hook entries should be removed (empty arrays) - assert_not_contains "$content" "claude-preview-diff.sh" "diff script should be removed" || return 1 - assert_not_contains "$content" "claude-close-diff.sh" "close script should be removed" || return 1 + assert_not_contains "$content" "code-preview-diff.sh" "diff script should be removed" || return 1 + assert_not_contains "$content" "code-close-diff.sh" "close script should be removed" || return 1 } # ── Test: Install is idempotent (no duplicates) ───────────────── test_install_idempotent() { - nvim_exec "require('claude-preview.hooks').install()" - nvim_exec "require('claude-preview.hooks').install()" + nvim_exec "require('code-preview.backends.claudecode').install()" + nvim_exec "require('code-preview.backends.claudecode').install()" sleep 0.2 local settings_file="$TEST_PROJECT_DIR/.claude/settings.local.json" @@ -64,7 +64,7 @@ test_install_idempotent() { # Count occurrences of the diff script — should be exactly 1 local count - count="$(echo "$content" | grep -o "claude-preview-diff.sh" | wc -l | tr -d ' ')" + count="$(echo "$content" | grep -o "code-preview-diff.sh" | wc -l | tr -d ' ')" assert_eq "1" "$count" "should have exactly one PreToolUse hook entry" || return 1 } @@ -84,7 +84,7 @@ test_install_preserves_existing() { } JSON - nvim_exec "require('claude-preview.hooks').install()" + nvim_exec "require('code-preview.backends.claudecode').install()" sleep 0.2 local content @@ -94,7 +94,7 @@ JSON assert_contains "$content" "permissions" "existing permissions should be preserved" || return 1 assert_contains "$content" "echo read" "existing hook should be preserved" || return 1 # Our hooks should also be present - assert_contains "$content" "claude-preview-diff.sh" "our hooks should be added" || return 1 + assert_contains "$content" "code-preview-diff.sh" "our hooks should be added" || return 1 } # ── Run all tests ──────────────────────────────────────────────── diff --git a/tests/backends/claude/test_stale_socket.sh b/tests/backends/claudecode/test_stale_socket.sh similarity index 95% rename from tests/backends/claude/test_stale_socket.sh rename to tests/backends/claudecode/test_stale_socket.sh index 7c640d4..b7ffd5f 100644 --- a/tests/backends/claude/test_stale_socket.sh +++ b/tests/backends/claudecode/test_stale_socket.sh @@ -39,7 +39,7 @@ EOF run_pretool_hook "$payload" scan >/dev/null sleep 0.5 local is_open - is_open="$(nvim_eval "require('claude-preview.diff').is_open()")" + is_open="$(nvim_eval "require('code-preview.diff').is_open()")" assert_eq "true" "$is_open" "diff should open on first instance" || return 1 run_posttool_hook "$payload" scan >/dev/null @@ -82,7 +82,7 @@ EOF sleep 0.5 local is_open2 - is_open2="$(nvim_eval "require('claude-preview.diff').is_open()")" + is_open2="$(nvim_eval "require('code-preview.diff').is_open()")" assert_eq "true" "$is_open2" "diff should open on second (restarted) instance" || return 1 run_posttool_hook "$payload2" scan >/dev/null @@ -125,7 +125,7 @@ EOF echo "$payload" | \ NVIM_LISTEN_ADDRESS= \ TMPDIR="$hook_tmpdir" \ - bash "$REPO_ROOT/bin/claude-preview-diff.sh" >/dev/null 2>&1 || exit_code=$? + bash "$REPO_ROOT/backends/claudecode/code-preview-diff.sh" >/dev/null 2>&1 || exit_code=$? # The hook exits 0 on non-crash paths, including when it cannot identify a # project-specific nvim instance. diff --git a/tests/backends/opencode/harness.ts b/tests/backends/opencode/harness.ts index 940426c..7d70102 100644 --- a/tests/backends/opencode/harness.ts +++ b/tests/backends/opencode/harness.ts @@ -30,7 +30,7 @@ async function main() { process.env.NVIM_LISTEN_ADDRESS = socket // Dynamic import of the plugin (it's the default export) - const pluginModule = await import(resolve(__dirname, "../../../opencode-plugin/index.ts")) + const pluginModule = await import(resolve(__dirname, "../../../backends/opencode/index.ts")) const pluginFactory = pluginModule.default // Initialize the plugin with the project directory diff --git a/tests/backends/opencode/test_edit.sh b/tests/backends/opencode/test_edit.sh index d0e5108..fc819fa 100644 --- a/tests/backends/opencode/test_edit.sh +++ b/tests/backends/opencode/test_edit.sh @@ -46,11 +46,11 @@ test_opencode_edit() { sleep 0.5 local is_open - is_open="$(nvim_eval "require('claude-preview.diff').is_open()")" + is_open="$(nvim_eval "require('code-preview.diff').is_open()")" assert_eq "true" "$is_open" "diff should be open after OpenCode edit_before" || return 1 local change_status - change_status="$(nvim_eval "require('claude-preview.changes').get('$test_file')")" + change_status="$(nvim_eval "require('code-preview.changes').get('$test_file')")" assert_eq "modified" "$change_status" "file should be marked as modified" || return 1 # Close via after hook @@ -58,11 +58,11 @@ test_opencode_edit() { sleep 0.5 local is_open_after - is_open_after="$(nvim_eval "require('claude-preview.diff').is_open()")" + is_open_after="$(nvim_eval "require('code-preview.diff').is_open()")" assert_eq "false" "$is_open_after" "diff should be closed after edit_after" || return 1 local changes_count - changes_count="$(nvim_eval "vim.tbl_count(require('claude-preview.changes').get_all())")" + changes_count="$(nvim_eval "vim.tbl_count(require('code-preview.changes').get_all())")" assert_eq "0" "$changes_count" "changes should be cleared" || return 1 } @@ -83,11 +83,11 @@ test_opencode_write_new() { sleep 0.5 local is_open - is_open="$(nvim_eval "require('claude-preview.diff').is_open()")" + is_open="$(nvim_eval "require('code-preview.diff').is_open()")" assert_eq "true" "$is_open" "diff should be open for OpenCode write" || return 1 local change_status - change_status="$(nvim_eval "require('claude-preview.changes').get('$new_file')")" + change_status="$(nvim_eval "require('code-preview.changes').get('$new_file')")" assert_eq "created" "$change_status" "new file should be marked as created" || return 1 run_opencode write_after "$TEST_SOCKET" "$TEST_PROJECT_DIR" "$new_file" >/dev/null 2>&1 @@ -112,14 +112,14 @@ test_opencode_bash_rm() { sleep 0.5 local change_status - change_status="$(nvim_eval "require('claude-preview.changes').get('$test_file')")" + change_status="$(nvim_eval "require('code-preview.changes').get('$test_file')")" assert_eq "deleted" "$change_status" "rm target should be marked as deleted" || return 1 run_opencode bash_after "$TEST_SOCKET" "$TEST_PROJECT_DIR" >/dev/null 2>&1 sleep 0.5 local change_after - change_after="$(nvim_eval "require('claude-preview.changes').get('$test_file') or 'nil'")" + change_after="$(nvim_eval "require('code-preview.changes').get('$test_file') or 'nil'")" assert_eq "nil" "$change_after" "deletion marker should be cleared" || return 1 } @@ -142,7 +142,7 @@ test_opencode_relative_path() { sleep 0.5 local is_open - is_open="$(nvim_eval "require('claude-preview.diff').is_open()")" + is_open="$(nvim_eval "require('code-preview.diff').is_open()")" assert_eq "true" "$is_open" "diff should open with relative path" || return 1 run_opencode edit_after "$TEST_SOCKET" "$TEST_PROJECT_DIR" "src/relative.lua" >/dev/null 2>&1 @@ -176,7 +176,7 @@ test_opencode_multi_file() { # After both before-hooks: file1's diff should be showing (it arrived first). local is_open_file1 - is_open_file1="$(nvim_eval "require('claude-preview.diff').is_open('$file1')")" + is_open_file1="$(nvim_eval "require('code-preview.diff').is_open('$file1')")" assert_eq "true" "$is_open_file1" "file1 diff should be showing after both before-hooks" || return 1 # Step 2: Close file1's diff via after-hook. @@ -185,7 +185,7 @@ test_opencode_multi_file() { # file1 closed → file2 should auto-show from the queue. local is_open_file2 - is_open_file2="$(nvim_eval "require('claude-preview.diff').is_open('$file2')")" + is_open_file2="$(nvim_eval "require('code-preview.diff').is_open('$file2')")" assert_eq "true" "$is_open_file2" "file2 diff should auto-show after file1 closes" || return 1 # Step 3: Close file2's diff via after-hook. @@ -194,11 +194,11 @@ test_opencode_multi_file() { # Everything should be closed now. local is_open_final - is_open_final="$(nvim_eval "require('claude-preview.diff').is_open()")" + is_open_final="$(nvim_eval "require('code-preview.diff').is_open()")" assert_eq "false" "$is_open_final" "diff should be closed after full cycle" || return 1 local changes_count - changes_count="$(nvim_eval "vim.tbl_count(require('claude-preview.changes').get_all())")" + changes_count="$(nvim_eval "vim.tbl_count(require('code-preview.changes').get_all())")" assert_eq "0" "$changes_count" "changes should be cleared after full cycle" || return 1 } diff --git a/tests/backends/opencode/test_install.sh b/tests/backends/opencode/test_install.sh index 5ce5954..8246632 100644 --- a/tests/backends/opencode/test_install.sh +++ b/tests/backends/opencode/test_install.sh @@ -12,7 +12,7 @@ nvim_exec "vim.cmd('cd $TEST_PROJECT_DIR')" # ── Test: Install OpenCode plugin ──────────────────────────────── test_install_opencode() { - nvim_exec "require('claude-preview.hooks').install_opencode()" + nvim_exec "require('code-preview.backends.opencode').install()" sleep 0.3 local target_dir="$TEST_PROJECT_DIR/.opencode/plugins" @@ -26,11 +26,11 @@ test_install_opencode() { test_uninstall_opencode() { # Install first - nvim_exec "require('claude-preview.hooks').install_opencode()" + nvim_exec "require('code-preview.backends.opencode').install()" sleep 0.2 # Uninstall - nvim_exec "require('claude-preview.hooks').uninstall_opencode()" + nvim_exec "require('code-preview.backends.opencode').uninstall()" sleep 0.2 local target_dir="$TEST_PROJECT_DIR/.opencode/plugins" diff --git a/tests/helpers.sh b/tests/helpers.sh index 6afb5cb..f944ec6 100755 --- a/tests/helpers.sh +++ b/tests/helpers.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# helpers.sh — Shared test utilities for claude-preview.nvim E2E tests +# helpers.sh — Shared test utilities for code-preview.nvim E2E tests # # Provides: # start_nvim — launch headless Neovim with the plugin on a known socket @@ -17,7 +17,7 @@ set -euo pipefail # ── Paths ──────────────────────────────────────────────────────── REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -TEST_SOCKET="${TMPDIR:-/tmp}/claude-preview-test-nvim.sock" +TEST_SOCKET="${TMPDIR:-/tmp}/code-preview-test-nvim.sock" TEST_PROJECT_DIR="" NVIM_PID="" @@ -38,7 +38,7 @@ TESTS_TOTAL=0 # ── Test project setup ─────────────────────────────────────────── setup_test_project() { - TEST_PROJECT_DIR="$(mktemp -d /tmp/claude-preview-test-project.XXXXXX)" + TEST_PROJECT_DIR="$(mktemp -d /tmp/code-preview-test-project.XXXXXX)" mkdir -p "$TEST_PROJECT_DIR" # Resolve symlinks so the path matches what lsof reports for nvim's cwd. # On macOS /tmp is a symlink to /private/tmp; without this the CWD-preference @@ -59,7 +59,7 @@ cleanup_test_project() { # of each test function that touches diffs or changes. reset_test_state() { # Close any open diff and clear changes inside the running Neovim - nvim_exec "require('claude-preview.diff').close_diff_and_clear()" 2>/dev/null || true + nvim_exec "require('code-preview.diff').close_diff_and_clear()" 2>/dev/null || true # Remove temp files that persist across runs (shared by both backends) local _tmpdir="${TMPDIR:-/tmp}" rm -f "$_tmpdir/claude-diff-original" "$_tmpdir/claude-diff-proposed" @@ -104,7 +104,7 @@ start_nvim_on_socket() { exec nvim --headless --clean --listen "$listen" \ --cmd "cd $nvim_cwd" \ --cmd "set rtp+=$repo_root" \ - -c "lua require('\''claude-preview'\'').setup()" + -c "lua require('\''code-preview'\'').setup()" ' _ "$nvim_cwd" "$REPO_ROOT" &>/dev/null & NVIM_PID=$! TEST_SOCKET="/tmp/nvim.${NVIM_PID}/0" @@ -175,8 +175,8 @@ nvim_eval() { local lua_expr="$1" local tmp_lua local tmp_out - tmp_lua="$(mktemp /tmp/claude-preview-test-eval.XXXXXX)" - tmp_out="/tmp/claude-preview-test-eval-out.$$" + tmp_lua="$(mktemp /tmp/code-preview-test-eval.XXXXXX)" + tmp_out="/tmp/code-preview-test-eval-out.$$" # Write Lua code that evaluates the expression and writes result to a temp file printf 'local __result = %s\nlocal __f = io.open("%s", "w")\n__f:write(tostring(__result))\n__f:close()' "$lua_expr" "$tmp_out" > "$tmp_lua" @@ -199,7 +199,7 @@ nvim_eval() { nvim_exec() { local lua_cmd="$1" local tmp_lua - tmp_lua="$(mktemp /tmp/claude-preview-test-exec.XXXXXX)" + tmp_lua="$(mktemp /tmp/code-preview-test-exec.XXXXXX)" printf '%s' "$lua_cmd" > "$tmp_lua" nvim --server "$TEST_SOCKET" --remote-expr "execute('luafile $tmp_lua')" >/dev/null 2>&1 local rc=$? @@ -302,11 +302,11 @@ run_pretool_hook() { if [[ "$mode" == "scan" ]]; then echo "$json_payload" | \ NVIM_LISTEN_ADDRESS= \ - bash "$REPO_ROOT/bin/claude-preview-diff.sh" 2>/dev/null || true + bash "$REPO_ROOT/backends/claudecode/code-preview-diff.sh" 2>/dev/null || true else echo "$json_payload" | \ NVIM_LISTEN_ADDRESS="$TEST_SOCKET" \ - bash "$REPO_ROOT/bin/claude-preview-diff.sh" 2>/dev/null || true + bash "$REPO_ROOT/backends/claudecode/code-preview-diff.sh" 2>/dev/null || true fi } @@ -318,10 +318,10 @@ run_posttool_hook() { if [[ "$mode" == "scan" ]]; then echo "$json_payload" | \ NVIM_LISTEN_ADDRESS= \ - bash "$REPO_ROOT/bin/claude-close-diff.sh" 2>/dev/null || true + bash "$REPO_ROOT/backends/claudecode/code-close-diff.sh" 2>/dev/null || true else echo "$json_payload" | \ NVIM_LISTEN_ADDRESS="$TEST_SOCKET" \ - bash "$REPO_ROOT/bin/claude-close-diff.sh" 2>/dev/null || true + bash "$REPO_ROOT/backends/claudecode/code-close-diff.sh" 2>/dev/null || true fi } diff --git a/tests/minimal_init.lua b/tests/minimal_init.lua index ef7d179..0811992 100644 --- a/tests/minimal_init.lua +++ b/tests/minimal_init.lua @@ -14,4 +14,4 @@ vim.opt.swapfile = false vim.cmd("runtime! plugin/plenary.vim") -- Load the plugin -require("claude-preview").setup() +require("code-preview").setup() diff --git a/tests/plugin/changes_registry_spec.lua b/tests/plugin/changes_registry_spec.lua index 98e006c..73f6ed9 100644 --- a/tests/plugin/changes_registry_spec.lua +++ b/tests/plugin/changes_registry_spec.lua @@ -1,6 +1,6 @@ -- changes_registry_spec.lua — Tests for the changes registry module -local changes = require("claude-preview.changes") +local changes = require("code-preview.changes") describe("changes registry", function() before_each(function() diff --git a/tests/plugin/diff_lifecycle_spec.lua b/tests/plugin/diff_lifecycle_spec.lua index 34cc604..339644f 100644 --- a/tests/plugin/diff_lifecycle_spec.lua +++ b/tests/plugin/diff_lifecycle_spec.lua @@ -1,7 +1,7 @@ -- diff_lifecycle_spec.lua — Tests for the diff module lifecycle -local diff = require("claude-preview.diff") -local changes = require("claude-preview.changes") +local diff = require("code-preview.diff") +local changes = require("code-preview.changes") -- Helper: write a temp file with content and return the path local function tmp_file(name, content) @@ -149,3 +149,82 @@ describe("diff lifecycle", function() os.remove(prop2) end) end) + +describe("diff layouts", function() + -- Temporarily override the diff layout for one test, restoring it afterwards. + local function with_layout(layout, fn) + local saved = require("code-preview").config.diff.layout + require("code-preview").config.diff.layout = layout + local ok, err = pcall(fn) + require("code-preview").config.diff.layout = saved + if not ok then error(err, 2) end + end + + before_each(function() + changes.clear_all() + diff.close_diff() + end) + + it("tab layout creates a new tab with two side-by-side windows", function() + local orig = tmp_file("tab_orig.txt", "line1\nline2") + local prop = tmp_file("tab_prop.txt", "line1\nchanged") + + local tabs_before = #vim.api.nvim_list_tabpages() + + with_layout("tab", function() + diff.show_diff(orig, prop, "layout_tab.txt") + end) + + assert.is_true(diff.is_open()) + -- A new tab should have been opened + assert.equals(tabs_before + 1, #vim.api.nvim_list_tabpages()) + -- The diff tab should have exactly 2 windows: CURRENT and PROPOSED + local diff_tabpage = vim.api.nvim_get_current_tabpage() + assert.equals(2, #vim.api.nvim_tabpage_list_wins(diff_tabpage)) + + diff.close_diff() + os.remove(orig) + os.remove(prop) + end) + + it("vsplit layout opens two windows in the current tab without creating a new tab", function() + local orig = tmp_file("vs_orig.txt", "alpha\nbeta") + local prop = tmp_file("vs_prop.txt", "alpha\ngamma") + + local tabs_before = #vim.api.nvim_list_tabpages() + + with_layout("vsplit", function() + diff.show_diff(orig, prop, "layout_vsplit.txt") + end) + + assert.is_true(diff.is_open()) + -- vsplit must NOT open a new tab + assert.equals(tabs_before, #vim.api.nvim_list_tabpages()) + + diff.close_diff() + os.remove(orig) + os.remove(prop) + end) + + it("inline layout creates a new tab with a single buffer (no side-by-side split)", function() + local orig = tmp_file("il_orig.txt", "hello\nworld") + local prop = tmp_file("il_prop.txt", "hello\nearth") + + local tabs_before = #vim.api.nvim_list_tabpages() + + with_layout("inline", function() + diff.show_diff(orig, prop, "layout_inline.txt") + end) + + assert.is_true(diff.is_open()) + -- inline also opens in a new tab + assert.equals(tabs_before + 1, #vim.api.nvim_list_tabpages()) + -- But only ONE window — no CURRENT/PROPOSED split + local diff_tabpage = vim.api.nvim_get_current_tabpage() + assert.equals(1, #vim.api.nvim_tabpage_list_wins(diff_tabpage)) + + diff.close_diff() + os.remove(orig) + os.remove(prop) + end) +end) diff --git a/tests/run.sh b/tests/run.sh index 469a18d..bae316f 100755 --- a/tests/run.sh +++ b/tests/run.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# run.sh — Main test runner for claude-preview.nvim E2E tests +# run.sh — Main test runner for code-preview.nvim E2E tests # # Usage: # ./tests/run.sh # run all tests (plugin + backends) @@ -116,7 +116,7 @@ main() { echo "" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" - echo -e "${CYAN}claude-preview.nvim E2E Test Suite${NC}" + echo -e "${CYAN}code-preview.nvim E2E Test Suite${NC}" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo ""