Compare commits

...

8 commits

Author SHA1 Message Date
e5412e464e
Cut 2.2.0 (#83)
Some checks failed
test / startup (macos-latest) (push) Has been cancelled
test / startup (ubuntu-latest) (push) Has been cancelled
test / shellcheck (push) Has been cancelled
test / docs (push) Has been cancelled
Promote the Unreleased changelog section to 2.2.0 and leave a fresh
Unreleased placeholder above it. No code changes.

Highlights of this release:
- Six default behaviors moved to opt-in flags: exrc/secure, normal-mode
  ,F reindent, jk escape, Ctrl-S save, sudo-save, completion keymaps,
  auto-pairs, terminal keymaps.
- install.sh: interactive PlugClean (no longer silently nukes
  user-added plugins), pipx-first Python tool install (no more
  silent --break-system-packages on PEP 668 distros), interactive
  profile selection, --dry-run / --configure-only flags.
- ttimeoutlen tuned to 50ms on SSH/TTY (was 10ms unconditionally).
- Built-in plugin guards corrected: gzip/logiPat/rrhelper/spellfile
  needed unscoped loaded_X form, not g:loaded_X.
- get.sh: hardened bootstrap (refuses non-chopsticks repos, dry-run
  is side-effect-free, CHOPSTICKS_DEST for non-default install path).
- :ChopsticksStatus diagnostic command, profile-aware ,? cheat sheet,
  scripts/test.sh shared by CI and local maintainers.
- README/install.sh now state Vim 8.1+ accurately; install summary
  leads with ,? as the first onboarding hint.
2026-05-17 14:30:47 +08:00
7f7a200b85
Fix built-in plugin guards for old-style runtime plugins (#82)
env.vim sets g:loaded_X for twelve built-in plugins to short-circuit
them. A prior fix corrected the casing on g:loaded_logiPat, but four
of those plugins actually check the unscoped loaded_X form, not
g:loaded_X — so those four guards have been doing nothing.

Affected (all old runtime plugins predating the g:loaded_ convention):

- gzip.vim       checks `loaded_gzip`
- logiPat.vim    checks `loaded_logiPat`
- rrhelper.vim   checks `loaded_rrhelper`
- spellfile.vim  checks `loaded_spellfile_plugin`

Set both forms (g: and unscoped) for those four — belt and
suspenders. Empirically saves ~270μs at startup, mostly from gzip
which was loading its full autocmd group on every launch.

Add a regression assertion: source .vimrc, verify the four unscoped
guards exist.

Closes #81
2026-05-17 14:08:20 +08:00
f0c662fd5c
Sweep stale Ctrl+P references after native-first cleanup (#80)
PR #36 rebound fuzzy-find from Ctrl+P (native command-line history)
to ,ff, but four user-facing sites still told people to press Ctrl+P:

- install.sh interactive menu (ripgrep description, fzf description)
- QUICKSTART.md "Find things" block
- .github/demo.tape (the script that regenerates the README hero GIF)

Three text fixes; the .tape update keeps a future regenerate honest.
The binary .github/demo.gif still shows Ctrl+P — regenerating it
requires the original /tmp/demo-project fixture which isn't in repo,
so that's left for a follow-up.

Closes #79
2026-05-17 13:31:36 +08:00
fc5cac570e
Bump ttimeoutlen to 50ms on SSH/TTY (#75)
* Bump ttimeoutlen to 50ms on SSH/TTY

ttimeoutlen=10 fragments multi-byte key codes (F-keys, arrows, Alt-
prefixes) when one-way latency exceeds 10ms, which is normal on SSH.
Vim then sees ESC followed by a literal letter and either returns to
Normal mode mid-insert or treats the trailing byte as a separate
keypress (so <F4> inserts "4").

50ms is the standard "slow link" value: well below perceptible <Esc>
delay locally and big enough for typical residential/long-haul SSH.
g:is_tty already gates several other terminal accommodations; reuse
it here.

Closes #74

* Pin TERM in ttimeoutlen test (env-hermetic)
2026-05-17 12:54:24 +08:00
5156543702
Bump Vim version docs to 8.1+ and lead onboarding with ,? (#78)
Two small first-run UX fixes bundled together (both touch install.sh
and README, with the same review surface):

1. README badge and install.sh warn line bumped 8.0+ → 8.1+. The
   runtime conditionally relies on patches 8.1.0360 (diffopt) and
   8.1.1517 (completeopt+=popup). Guards no-op on 8.0 but the docs
   were silently misleading anyone debugging on stale Vim.

2. "First steps inside Vim" block reordered so ,? leads. The cheat
   sheet is the most useful onboarding asset; it shouldn't be buried
   below three "how to leave" hints. Also promoted ,x above :q!
   since the latter is emergency, not first-run.

Closes #76
Closes #77
2026-05-17 11:00:03 +08:00
74cc479a0e
Insulate vim smoke tests from a user's local chopsticks.vim (#73)
Four `vim -u NONE` invocations in scripts/test.sh set a profile or
option as their first `-c` then `source .vimrc`. They didn't override
XDG_CONFIG_HOME, so .vimrc's local-config hook read
~/.config/chopsticks.vim and any `let g:chopsticks_profile = '...'`
there silently overrode the test's setup.

After install.sh runs (writing `engineer` or `full` to that file),
scripts/test.sh vim fails on the developer's machine but passes in
CI — CI never runs install.sh.

Fix by prepending XDG_CONFIG_HOME="\$EMPTY_XDG" to the four bare sites,
matching what the surrounding tests already do. L176 stays unchanged
because it explicitly sets g:chopsticks_local_config to its own
controlled file.

Closes #72
2026-05-17 09:39:12 +08:00
debcdd33c4
Prefer pipx over pip3 --break-system-packages for Python tools (#67)
The previous fallback chain (pip3 install → pip3 install
--break-system-packages) silently writes into the distro Python's
site-packages on PEP 668 systems (Debian 12+, Ubuntu 23.04+). The warn
message fires after the damage is done. Since the target audience is
engineers SSH-ing into Linux servers, the PEP 668 hit rate is high.

New order:
  1. pipx install   (isolated per-tool venvs)
  2. pip3 --user    (works on pre-PEP-668 Python)
  3. --break-system-packages  (gated behind CHOPSTICKS_ALLOW_BREAK_SYSTEM=1)
  4. fail with a remediation hint

Also:
- Bootstrap pipx via the system package manager if it's missing.
- Warn if $HOME/.local/bin is absent from PATH (pipx and pip --user
  both install there).

Closes #66
2026-05-17 08:42:52 +08:00
d6df4fee3e
Make PlugClean interactive in install.sh (#65)
Previously install.sh ran `_vim_run 'PlugClean!'` unconditionally before
PlugInstall. PlugClean with bang removes any directory under
~/.vim/plugged not declared in the active profile's g:plugs, without
confirmation — silently nuking plugins a user has manually cloned to try
out, plus leftovers from older profile config (Goyo, Limelight, etc.).

Replace with a `list_extra_plugins` helper that runs a Vim subprocess
with all opt-in flags forced on and `$TMUX = '1'`, so the declared set
covers every plugin any chopsticks profile/flag combination could
register. Conditional plugins (vim-tmux-navigator, auto-pairs) are
therefore never flagged as extras.

When extras are detected:
- interactive: list them, ask before running PlugClean!
- --yes: leave in place, instruct the user to run :PlugClean manually
- otherwise (no extras): skip PlugClean entirely (saves an 80ms Vim
  launch on the common case)

Closes #64
2026-05-17 08:38:35 +08:00
8 changed files with 150 additions and 29 deletions

4
.github/demo.tape vendored
View file

@ -24,8 +24,8 @@ Type "vim server.py"
Enter
Sleep 3.5s
# ── 2. Fuzzy find files (Ctrl+p → type → select) ──────────────────────────
Ctrl+p
# ── 2. Fuzzy find files (,ff → type → select) ────────────────────────────
Type ",ff"
Sleep 1.5s
Type "route"
Sleep 2.5s

View file

@ -2,6 +2,8 @@
## Unreleased
## 2.2.0 — 2026-05-17
### Added
- `~/.config/chopsticks.vim` local pre-load config for profile and user choices
@ -23,6 +25,30 @@
### Fixed
- Built-in plugin guards for `gzip`, `logiPat`, `rrhelper`, and
`spellfile_plugin` are now set in both `g:`-prefixed and unscoped
forms — those four old-style runtime plugins check `loaded_X` (no
`g:`), so the previous `g:loaded_X` lines did nothing. Saves ~270μs
total at startup, mostly from gzip
- `install.sh` interactive menu and QUICKSTART now show `,ff` (current
fuzzy-find binding) instead of the stale `Ctrl+p` left over from the
native-first cleanup; `.github/demo.tape` updated to match (the
binary GIF still shows `Ctrl+p` until someone regenerates it from a
fresh fixture)
- README badge and `install.sh` recommend Vim 8.1+ instead of 8.0+ —
the runtime conditionally relies on patches `8.1.0360` (diffopt) and
`8.1.1517` (completeopt+=popup), so 8.0 users hit option errors
- `ttimeoutlen` is now 50ms when `g:is_tty` (was 10ms unconditionally);
fixes F-keys, arrow keys, and Alt-prefixes fragmenting on SSH where
one-way latency exceeds 10ms. Local terminals keep the 10ms snappy
default
- `install.sh` no longer silently `PlugClean!`s user-added plugins from
`~/.vim/plugged`; it now lists undeclared plugin directories first and
asks before removing them (`--yes` skips the removal entirely)
- `install.sh` Python tools now prefer `pipx` and `pip3 --user` over
`pip3 install --break-system-packages`; the break-system path is gated
behind `CHOPSTICKS_ALLOW_BREAK_SYSTEM=1` so PEP 668 distros are no
longer silently polluted
- `g:loaded_logipat` typo → `g:loaded_logiPat` — logiPat was loading fully (0.478ms wasted)
- `get.sh` now refuses to update an existing `~/.vim` git repo unless its
origin is chopsticks
@ -33,6 +59,9 @@
### Changed
- `install.sh` "First steps inside Vim" block now leads with `,?`
(cheat sheet) — the single best onboarding asset is now the first
thing a new user sees after install, not the fourth
- `set exrc`/`set secure` are now opt-in via `g:chopsticks_enable_exrc = 1`;
Vim no longer sources project-local `.vimrc`/`.exrc` from the working
directory by default

View file

@ -45,7 +45,7 @@ Esc back to Normal
## Find things
```
Ctrl+p fuzzy find file (git-aware)
,ff fuzzy find file (git-aware)
,rg ripgrep project
,b search buffers
,fh recent files

View file

@ -10,7 +10,7 @@
<p align="center">
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square" alt="MIT License"></a>
<a href="https://www.vim.org/"><img src="https://img.shields.io/badge/Vim-8.0%2B-brightgreen?style=flat-square" alt="Vim 8.0+"></a>
<a href="https://www.vim.org/"><img src="https://img.shields.io/badge/Vim-8.1%2B-brightgreen?style=flat-square" alt="Vim 8.1+"></a>
<a href="#install"><img src="https://img.shields.io/badge/platform-macOS%20%7C%20Linux-lightgrey?style=flat-square" alt="Platform"></a>
<a href="https://github.com/m1ngsama/chopsticks/actions"><img src="https://img.shields.io/github/actions/workflow/status/m1ngsama/chopsticks/test.yml?style=flat-square&label=tests" alt="Tests"></a>
<a href="https://github.com/m1ngsama/chopsticks/releases"><img src="https://img.shields.io/github/v/release/m1ngsama/chopsticks?style=flat-square&color=orange" alt="Release"></a>

View file

@ -519,7 +519,7 @@ fi
[[ -n "$VIM_BIN" ]] || die "vim installed but not found in PATH"
ok "Found: $("$VIM_BIN" --version | head -n1) ($VIM_BIN)"
"$VIM_BIN" --version | grep -q 'Vi IMproved 8\|Vi IMproved 9' || \
warn "Vim 8.0+ recommended for full async/LSP support — some features may not work"
warn "Vim 8.1+ recommended for full async/LSP support — some features may not work"
# Node.js (optional — vim-lsp needs no Node.js; only npm formatters do)
HAS_NODE=0; command -v node >/dev/null 2>&1 && HAS_NODE=1
@ -656,10 +656,59 @@ VIMEOF
fi
}
if [[ -d "$HOME/.vim/plugged" ]] && [[ -n "$(find "$HOME/.vim/plugged" -mindepth 1 -maxdepth 1 2>/dev/null)" ]]; then
warn "PlugClean: removing plugins not listed in .vimrc from ~/.vim/plugged"
# Echoes one name per line for each directory under ~/.vim/plugged that no
# chopsticks profile/flag combination would declare. The detection script
# bypasses local config and forces every opt-in on, so a plugin that's only
# loaded inside tmux or behind a flag does NOT show up as "extra". Empty
# output means PlugClean would be a no-op for the user's tree.
list_extra_plugins() {
local declared_file="$_TMPDIR/declared-plugins.txt"
local declare_script="$_TMPDIR/declared-plugins.vim"
local plugged="$HOME/.vim/plugged"
[[ -d "$plugged" ]] || return 0
cat > "$declare_script" <<'VIMEOF'
let g:chopsticks_local_config = '/dev/null'
let $TMUX = '1'
let g:chopsticks_profile = 'full'
let g:chopsticks_enable_auto_pairs = 1
let g:chopsticks_enable_terminal_keymaps = 1
execute 'source ' . fnameescape($CHOPSTICKS_VIMRC)
if !exists('g:plugs')
cquit
endif
call writefile(sort(keys(g:plugs)), $CHOPSTICKS_DECLARED_PLUGINS)
qa!
VIMEOF
CHOPSTICKS_VIMRC="$SCRIPT_DIR/.vimrc" \
CHOPSTICKS_DECLARED_PLUGINS="$declared_file" \
"$VIM_BIN" -u NONE -i NONE -es -N -S "$declare_script" >/dev/null 2>&1 || return 1
[[ -e "$declared_file" ]] || return 0
local plugin name
while IFS= read -r plugin; do
[[ -d "$plugin" ]] || continue
name="$(basename "$plugin")"
if ! grep -Fxq "$name" "$declared_file"; then
printf '%s\n' "$name"
fi
done < <(find "$plugged" -mindepth 1 -maxdepth 1 -type d 2>/dev/null | sort)
}
extras="$(list_extra_plugins || true)"
if [[ -n "$extras" ]]; then
warn "Plugins under ~/.vim/plugged that the active profile does not declare:"
while IFS= read -r ex; do
[[ -n "$ex" ]] && echo " - $ex"
done <<< "$extras"
if [[ $AUTO_YES -eq 1 ]]; then
info "Leaving them in place (--yes mode). Run :PlugClean inside Vim to remove."
elif ask "Remove these directories with PlugClean!?"; then
_vim_run 'PlugClean!' || true # plug.vim may exit non-zero after removal; harmless
else
info "Leaving them in place. Run :PlugClean inside Vim if you change your mind."
fi
fi
_vim_run 'PlugClean!' || true # remove plugins no longer in vimrc; ignore exit code (none expected)
_vim_run 'PlugInstall --sync' || true # fzf post-install hook may exit non-zero; harmless
verify_plugins || die "Plugin installation failed — retry with a stable network connection."
@ -699,11 +748,11 @@ fi
# ── System tools ─────────────────────────────────────────────────────────────
if [[ $HAS_PKG_MGR -eq 1 ]]; then
_I_RIPGREP=$_idx
_ITEMS+=("ripgrep|,rg / ,rG project-wide search · powers Ctrl+p preview|1")
_ITEMS+=("ripgrep|,rg / ,rG project-wide search · powers FZF preview|1")
: $(( _idx++ ))
_I_FZF=$_idx
_ITEMS+=("fzf|Ctrl+p fuzzy file search · ,b buffers · ,rt tag search|1")
_ITEMS+=("fzf|,ff fuzzy file search · ,b buffers · ,rt tag search|1")
: $(( _idx++ ))
_I_CTAGS=$_idx
@ -966,19 +1015,45 @@ elif [[ $_I_PYTHON -lt 0 ]] || ! _selected "$_I_PYTHON"; then
skip "Python tool suite (skipped by user)"
SKIPPED+=("black" "isort" "flake8" "pylint" "yamllint" "sqlfluff")
else
# Prefer pipx (isolated per-tool venvs, the recommended path on PEP 668
# distros like Debian 12+/Ubuntu 23.04+). Bootstrap pipx via the system
# package manager if missing. Only fall back to --break-system-packages
# if the user explicitly opts in.
if ! command -v pipx >/dev/null 2>&1; then
if pkg_install pipx pipx python-pipx pipx 2>/dev/null; then
ok "pipx (recommended Python CLI installer)"
else
warn "pipx not available — falling back to pip3 --user"
fi
fi
case ":$PATH:" in
*":$HOME/.local/bin:"*) ;;
*) warn "\$HOME/.local/bin is not on PATH — pipx/pip --user tools will not be found"
info "Add to your shell rc: export PATH=\"\$HOME/.local/bin:\$PATH\"" ;;
esac
pip_install() {
local pkg="$1" check="${2:-$1}"
if command -v "$check" >/dev/null 2>&1; then
ok "$pkg (already installed)"; return
fi
if pip3 install --quiet "$pkg" 2>/dev/null; then
ok "$pkg"; INSTALLED+=("$pkg")
elif pip3 install --quiet --break-system-packages "$pkg" 2>/dev/null; then
warn "$pkg installed with --break-system-packages (consider using a virtualenv)"
ok "$pkg"; INSTALLED+=("$pkg")
else
fail "$pkg"; FAILED+=("$pkg")
if command -v pipx >/dev/null 2>&1 && pipx install --quiet "$pkg" 2>/dev/null; then
ok "$pkg (via pipx)"; INSTALLED+=("$pkg")
return
fi
if pip3 install --quiet --user "$pkg" 2>/dev/null; then
ok "$pkg (via pip --user)"; INSTALLED+=("$pkg")
return
fi
if [[ "${CHOPSTICKS_ALLOW_BREAK_SYSTEM:-0}" == "1" ]] && \
pip3 install --quiet --break-system-packages "$pkg" 2>/dev/null; then
warn "$pkg installed with --break-system-packages (CHOPSTICKS_ALLOW_BREAK_SYSTEM=1)"
INSTALLED+=("$pkg")
return
fi
fail "$pkg — install pipx, or set CHOPSTICKS_ALLOW_BREAK_SYSTEM=1 to bypass PEP 668"
FAILED+=("$pkg")
}
pip_install black
pip_install isort
@ -1126,10 +1201,10 @@ echo -e " ${CYAN}vim .${NC} Open dashboard in current directory"
echo -e " ${CYAN}vim myfile${NC} Edit a specific file"
echo ""
echo -e "${BOLD} First steps inside Vim${NC}"
echo -e " ${CYAN},?${NC} Open cheat sheet — your map of every keybinding"
echo -e " ${CYAN}Esc${NC} Exit insert mode → back to Normal"
echo -e " ${CYAN}:q!${NC} + Enter Emergency quit without saving"
echo -e " ${CYAN},x${NC} Save and quit"
echo -e " ${CYAN},?${NC} Open cheat sheet"
echo -e " ${CYAN}:q!${NC} + Enter Emergency quit without saving"
if [[ $CONFIG_PROFILE != "minimal" ]]; then
echo -e " ${CYAN}:LspInstallServer${NC} Install LSP for current filetype"
fi

View file

@ -42,7 +42,10 @@ set noerrorbells
set novisualbell
set t_vb=
set ttimeout
set ttimeoutlen=10
" Wait long enough on SSH/TTY for multi-byte key codes to arrive intact —
" 10ms fragments F-keys, arrows, and Alt-prefixes when one-way latency > 10ms.
" 50ms is well under perceptible <Esc>→Normal delay locally.
let &ttimeoutlen = g:is_tty ? 50 : 10
if $COLORTERM ==# 'gnome-terminal'
set t_Co=256

View file

@ -42,16 +42,18 @@ let g:chopsticks_markdown_conceal = get(g:, 'chopsticks_markdown_conceal',
let g:chopsticks_lsp_virtual_text = get(g:, 'chopsticks_lsp_virtual_text',
\ s:profile_full && !g:is_tty)
" Skip built-in plugins we never use
" Skip built-in plugins we never use.
" Modern plugins check g:loaded_X; older ones (gzip, logiPat, rrhelper,
" spellfile) check the unscoped loaded_X form, so we set both.
let g:loaded_2html_plugin = 1
let g:loaded_getscriptPlugin = 1
let g:loaded_gzip = 1
let g:loaded_logiPat = 1
let g:loaded_rrhelper = 1
let g:loaded_gzip = 1 | let loaded_gzip = 1
let g:loaded_logiPat = 1 | let loaded_logiPat = 1
let g:loaded_rrhelper = 1 | let loaded_rrhelper = 1
let g:loaded_tarPlugin = 1
let g:loaded_vimballPlugin = 1
let g:loaded_zipPlugin = 1
let g:loaded_tutor_mode_plugin = 1
let g:loaded_spellfile_plugin = 1
let g:loaded_spellfile_plugin = 1 | let loaded_spellfile_plugin = 1
let g:loaded_openPlugin = 1
let g:loaded_manpager_plugin = 1

View file

@ -165,7 +165,7 @@ check_vim() {
XDG_CONFIG_HOME="$EMPTY_XDG" vim -u "$TMP_ROOT/chopsticks path/.vimrc" \
-i NONE -es -N -c 'qa!' 2>&1
vim -u NONE -i NONE -es -N \
XDG_CONFIG_HOME="$EMPTY_XDG" vim -u NONE -i NONE -es -N \
-c 'let g:chopsticks_profile = "minimal"' \
-c 'source .vimrc' \
-c 'if has_key(g:plugs, "ale") || has_key(g:plugs, "vim-lsp") || has_key(g:plugs, "vim-lsp-settings") || has_key(g:plugs, "asyncomplete.vim") || has_key(g:plugs, "auto-pairs") | cquit | endif' \
@ -257,6 +257,18 @@ check_vim() {
-c 'if maparg(",F", "n") !~# "gg=G" | cquit | endif' \
-c 'qa!' 2>&1
TERM=xterm-256color XDG_CONFIG_HOME="$EMPTY_XDG" vim -u .vimrc -i NONE -es -N \
-c 'if g:is_tty || &ttimeoutlen != 10 | cquit | endif' \
-c 'qa!' 2>&1
TERM=linux XDG_CONFIG_HOME="$EMPTY_XDG" vim -u .vimrc -i NONE -es -N \
-c 'if !g:is_tty || &ttimeoutlen != 50 | cquit | endif' \
-c 'qa!' 2>&1
XDG_CONFIG_HOME="$EMPTY_XDG" vim -u .vimrc -i NONE -es -N \
-c 'if !exists("loaded_gzip") || !exists("loaded_logiPat") || !exists("loaded_rrhelper") || !exists("loaded_spellfile_plugin") | cquit | endif' \
-c 'qa!' 2>&1
XDG_CONFIG_HOME="$EMPTY_XDG" vim -u .vimrc -i NONE -es -N \
-c 'silent! delcommand LspStatus' \
-c 'silent! delcommand LspInstallServer' \
@ -267,7 +279,7 @@ check_vim() {
-c 'qa!' 2>&1
grep -Fq 'OK vim-lsp stack (installed; not loaded yet)' "$TMP_ROOT/status-lsp-not-loaded.txt"
vim -u NONE -i NONE -es -N \
XDG_CONFIG_HOME="$EMPTY_XDG" vim -u NONE -i NONE -es -N \
-c 'let g:chopsticks_profile = "minimal"' \
-c 'source .vimrc' \
-c 'ChopsticksStatus' \
@ -322,7 +334,7 @@ check_vim() {
exit 1
fi
vim -u NONE -i NONE -es -N \
XDG_CONFIG_HOME="$EMPTY_XDG" vim -u NONE -i NONE -es -N \
-c 'let g:chopsticks_profile = "minimal"' \
-c 'source .vimrc' \
-c 'normal ,?' \
@ -347,7 +359,7 @@ check_vim() {
-c 'if !&swapfile || !&writebackup || &directory !~# "\.vim/.swap" | cquit | endif' \
-c 'qa!' 2>&1
vim -u NONE -i NONE -es -N \
XDG_CONFIG_HOME="$EMPTY_XDG" vim -u NONE -i NONE -es -N \
-c 'let g:ale_fix_on_save = 0' \
-c 'source .vimrc' \
-c 'if g:ale_fix_on_save != 0 | cquit | endif' \