Compare commits

..

12 commits

Author SHA1 Message Date
3507945b78 Prepare 2.3.0 release candidate
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
2026-05-24 17:41:55 +08:00
16c1974e1a Add ergonomic Vim window navigation 2026-05-24 09:45:32 +08:00
d3a137a36e Align docs with expert Vim workflow 2026-05-24 09:02:50 +08:00
a6a101a286 Update checkout action for Node 24 2026-05-23 23:25:43 +08:00
fd29dd160e Add local config commands 2026-05-23 20:00:00 +08:00
0b1c8d94d7 Add native Vim help entrypoint 2026-05-23 19:48:15 +08:00
a4ea56525f Add beta documentation consistency check 2026-05-23 19:38:48 +08:00
87719ebdd3 Add in-editor beta guide 2026-05-23 19:27:01 +08:00
c5f84a700d Prepare v3 beta candidate docs 2026-05-23 18:08:31 +08:00
3f967af8e4 Prepare v3 ergonomic Space keymap candidate 2026-05-21 12:54:13 +08:00
f38bdf6e57 Revert "Release v3.0.0 ergonomic keymap"
This reverts commit d56ca80da7.
2026-05-21 12:02:55 +08:00
d56ca80da7 Release v3.0.0 ergonomic keymap 2026-05-21 11:50:32 +08:00
41 changed files with 2742 additions and 1021 deletions

View file

@ -10,4 +10,4 @@
- [ ] `vim --startuptime` shows no regression
- [ ] Tested on macOS / Linux
- [ ] `,?` cheat sheet still accurate
- [ ] `SPC ?` cheat sheet still accurate

14
.github/demo-project/middleware.py vendored Normal file
View file

@ -0,0 +1,14 @@
"""Small helpers for the README demo server."""
def apply_cors(headers):
"""Return response headers with permissive demo CORS."""
next_headers = dict(headers)
next_headers["Access-Control-Allow-Origin"] = "*"
next_headers["Access-Control-Allow-Headers"] = "Content-Type"
return next_headers
def log_request(path):
"""Format a deterministic request log line."""
return f"GET {path}"

20
.github/demo-project/report.py vendored Normal file
View file

@ -0,0 +1,20 @@
"""Small runnable report for the README demo."""
import json
from routes import get_users, health_check
def build_summary():
"""Collect a deterministic status payload."""
users = get_users()
return {
"service": "demo-api",
"status": health_check()["status"],
"active_users": users["total"],
"roles": sorted({user["role"] for user in users["users"]}),
}
if __name__ == "__main__":
print(json.dumps(build_summary(), indent=2))

31
.github/demo-project/routes.py vendored Normal file
View file

@ -0,0 +1,31 @@
"""URL route definitions and handler functions."""
from datetime import datetime
def health_check():
"""Return server health status."""
return {
"status": "ok",
"uptime": "3d 14h 22m",
"version": "1.2.0",
}
def get_users():
"""Return list of active users."""
return {
"users": [
{"id": 1, "name": "alice", "role": "admin"},
{"id": 2, "name": "bob", "role": "engineer"},
{"id": 3, "name": "carol", "role": "engineer"},
],
"total": 3,
"generated_at": datetime(2026, 5, 21, 12, 0, 0).isoformat(),
}
ROUTES = {
"/health": health_check,
"/users": get_users,
}

58
.github/demo-project/server.py vendored Normal file
View file

@ -0,0 +1,58 @@
"""Lightweight HTTP server with routing and middleware."""
import json
import logging
import os
from datetime import datetime
from http.server import BaseHTTPRequestHandler, HTTPServer
from middleware import apply_cors, log_request
from routes import ROUTES
logger = logging.getLogger(__name__)
def dispatch(path):
"""Route request to the matching handler."""
logger.info(log_request(path))
handler = ROUTES.get(path)
if handler:
data = handler()
return 200, json.dumps(data)
return 404, json.dumps({"error": "not found"})
def respond(status, body):
"""Format an HTTP response with CORS headers."""
headers = apply_cors({})
headers["Content-Type"] = "application/json"
logger.info("%d (%d bytes)", status, len(body))
return {"status": status, "headers": headers, "body": body}
class DemoHandler(BaseHTTPRequestHandler):
"""Tiny request handler for the README GIF."""
def do_GET(self):
status, body = dispatch(self.path)
response = respond(status, body)
self.send_response(response["status"])
for name, value in response["headers"].items():
self.send_header(name, value)
self.end_headers()
self.wfile.write(response["body"].encode("utf-8"))
def log_message(self, *_args):
return
def main():
"""Start the server."""
port = int(os.environ.get("CHOPSTICKS_DEMO_PORT", "8080"))
print(f"[{datetime.now():%H:%M:%S}] server running on :{port}")
print(f"routes: {', '.join(ROUTES.keys())}")
HTTPServer(("127.0.0.1", port), DemoHandler).serve_forever()
if __name__ == "__main__":
main()

BIN
.github/demo.gif vendored

Binary file not shown.

Before

Width:  |  Height:  |  Size: 990 KiB

After

Width:  |  Height:  |  Size: 1.1 MiB

98
.github/demo.tape vendored
View file

@ -1,8 +1,14 @@
# chopsticks demo — "what can I do with this?"
# chopsticks demo — for people who already know Vim:
# show the project workflow layer Vim does not assemble by default.
# project loop: visible jump → files → run → grep → git → active key help.
# Rule: NEVER use Escape to close FZF — always Enter (deterministic).
# Pacing: hold each scene long enough for the viewer to read.
Output .github/demo.gif
Require bash
Require vim
Require python3
Require git
Set Shell bash
Set FontSize 14
Set Width 1080
@ -11,53 +17,83 @@ Set Theme "Builtin Solarized Dark"
Set TypingSpeed 50ms
Set Padding 10
# ── 0. Start API server in background ──────────────────────────────────────
Type "cd /tmp/demo-project"
# ── 0. Prepare deterministic fixture and local config ─────────────────────
Hide
Type "export CHOPSTICKS_ROOT=$(git rev-parse --show-toplevel)"
Enter
Type "export XDG_CONFIG_HOME=$(mktemp -d /tmp/chopsticks-demo-xdg-XXXXXX)"
Enter
Type "export CHOPSTICKS_DEMO_PROJECT=$(mktemp -d /tmp/chopsticks-demo-project-XXXXXX)"
Enter
Type `cp "$CHOPSTICKS_ROOT/.github/demo-project/"*.py "$CHOPSTICKS_DEMO_PROJECT/"`
Enter
Type "cd $CHOPSTICKS_DEMO_PROJECT"
Enter
Type "git init -q && git symbolic-ref HEAD refs/heads/main && git add ."
Enter
Type `clear && printf "%s\n" "chopsticks: a long-term Vim efficiency kit" "" "For experienced Vim users who already know how to edit." "Keep Vim's language. Standardize the project loop:" "" " s + 2 chars visible jump on the current screen" " SPC SPC project-aware file switcher" " SPC rr run the current file" " SPC / ripgrep the project" " SPC gs git status, with no push/pull hotkeys" " SPC ? active keys explained inside Vim"`
Enter
Show
Sleep 6s
Type "clear"
Enter
Type `vim -Nu "$CHOPSTICKS_ROOT/.vimrc" server.py`
Enter
Wait+Screen /server.py/
Sleep 1s
# ── 1. Screen-local motion: stop counting hjkl across dense code ───────────
Type "s"
Sleep 0.5s
Type "python3 serve.py > /dev/null 2>&1 &"
Type "di"
Sleep 2s
Ctrl+C
Sleep 1s
# ── 2. Project files: one habit for git-aware file switching ───────────────
Space 2
Sleep 1.5s
Type "report"
Sleep 2s
Enter
Sleep 3s
# ── 3. Edit-run loop: run the current file without leaving Vim ─────────────
Space
Type "rr"
Sleep 4s
Enter
Sleep 1s
# ── 1. Open file — syntax highlighting + statusline ────────────────────────
Type "vim server.py"
Enter
Sleep 3.5s
# ── 2. Fuzzy find files (,ff → type → select) ────────────────────────────
Type ",ff"
Sleep 1.5s
Type "route"
# ── 4. Project grep: ripgrep as part of the same muscle-memory layer ───────
Space
Type "/"
Sleep 1s
Type "def get_users"
Sleep 2.5s
Enter
Sleep 3s
# ── 3. Ripgrep project search (,rg → query → select) ──────────────────────
Type ",rg"
Sleep 1.5s
Type "def "
# ── 5. Git boundary: status is fast; push/pull stay explicit commands ──────
Space
Type "gs"
Sleep 3s
Enter
Sleep 3s
# ── 4. Curl the running API from inside Vim ────────────────────────────────
Type ":!curl -s localhost:8080/users | python3 -m json.tool"
Enter
Sleep 4.5s
Enter
Sleep 1.5s
# ── 5. Cheat sheet (,?) ───────────────────────────────────────────────────
# Reset to server.py so cheat sheet shows code on left, keys on right.
Type ":edit server.py"
Type ":bd!"
Enter
Sleep 1s
Type ",?"
# ── 6. Self-documenting config: no private wiki required ───────────────────
Space
Type "?"
Sleep 5.5s
Type "q"
Sleep 0.5s
# ── done ───────────────────────────────────────────────────────────────────
Hide
Type ":qa!"
Enter
Sleep 0.5s
Type `test -n "$CHOPSTICKS_DEMO_PROJECT" && rm -rf "$CHOPSTICKS_DEMO_PROJECT"`
Enter
Sleep 0.5s

View file

@ -13,7 +13,7 @@ jobs:
os: [ubuntu-latest, macos-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: Install Vim
timeout-minutes: 5
@ -61,7 +61,7 @@ jobs:
shellcheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: Check test runner CLI
run: |
scripts/test.sh --help
@ -72,7 +72,7 @@ jobs:
docs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: Install markdownlint
run: npm install -g markdownlint-cli
- name: Lint Markdown

2
.gitignore vendored
View file

@ -1,8 +1,10 @@
*.swp
*.swo
.DS_Store
__pycache__/
Session.vim
autoload/
plugged/
doc/tags
.swap/
.undo/

16
.vimrc
View file

@ -1,9 +1,15 @@
set nocompatible
let g:chopsticks_dir = fnamemodify(resolve(expand('<sfile>')), ':h')
if index(split(&runtimepath, ','), g:chopsticks_dir) < 0
let &runtimepath = g:chopsticks_dir . ',' . &runtimepath
endif
let s:xdg_config_home = !empty($XDG_CONFIG_HOME) && $XDG_CONFIG_HOME =~# '^/'
\ ? $XDG_CONFIG_HOME
\ : '~/.config'
let s:local_config = expand(get(g:, 'chopsticks_local_config',
\ s:xdg_config_home . '/chopsticks.vim'))
let g:chopsticks_resolved_local_config = s:local_config
if filereadable(s:local_config)
execute 'source ' . fnameescape(s:local_config)
endif
@ -27,4 +33,14 @@ call s:load('lsp')
call s:load('lint')
call s:load('git')
call s:load('languages')
call s:load('buffers')
call s:load('utilities')
call s:load('files')
call s:load('runner')
call s:load('quickfix')
call s:load('status')
call s:load('cheatsheet')
call s:load('tutor')
call s:load('beta')
call s:load('help')
call s:load('tools')

104
BETA.md Normal file
View file

@ -0,0 +1,104 @@
# 2.3.0 Release Candidate Testing
This branch is the 2.3.0 release candidate. The goal is to prove that the Space
layout can serve as the project loop for experienced Vim users, not just that
the mappings work in isolation. Do not tag or publish it as `2.3.0` until the
checklist below is closed.
Inside Vim, run `:ChopsticksBeta` for the compact checklist,
`:ChopsticksBetaLog` for editable local notes, and `:ChopsticksBetaSession`
to append a new session block. Run `:ChopsticksHelp` or `:help chopsticks`
for the native Vim reference.
## Install the release candidate
Existing checkout:
```bash
cd ~/.vim
git fetch origin
git checkout release/2.3.0
git pull --ff-only
vim -Nu ~/.vimrc -n -es +'PlugInstall --sync' +'qa!'
```
Fresh checkout:
```bash
git clone --branch release/2.3.0 https://github.com/m1ngsama/chopsticks.git ~/.vim
ln -sf ~/.vim/.vimrc ~/.vimrc
vim -Nu ~/.vimrc -n -es +'PlugInstall --sync' +'qa!'
```
Keep local choices in `${XDG_CONFIG_HOME:-~/.config}/chopsticks.vim`:
```vim
let g:chopsticks_profile = 'engineer'
let g:chopsticks_keymap_style = 'space'
```
Inside Vim, `:ChopsticksConfig` opens that file and `:ChopsticksReload`
reloads chopsticks after saving it.
## Daily test loop
Use the release candidate for real editing, not only demos. A session should
exercise the trained loop until it either feels automatic or exposes friction.
For each session, record:
- The task: project navigation, code edit, grep, git, LSP, Markdown, SSH.
- The first key you tried when you got stuck.
- Whether `SPC ?`, `:ChopsticksTutor`, or `:ChopsticksStatus` answered it.
- Any mapping that felt slow, awkward, surprising, or too easy to mistype.
- Any documentation line that was wrong, missing, or redundant.
`:ChopsticksBetaLog` opens `${XDG_CONFIG_HOME:-~/.config}/chopsticks-2.3.0.md`
by default. Set `g:chopsticks_beta_log` before loading chopsticks to use a
different path. Use `:ChopsticksBetaSession` at the start of each real editing
session so every test has a timestamped block.
## Workflows to exercise
```text
SPC SPC find file SPC / grep project
s + 2ch jump on screen gd / gr definition / references
SPC rr run current file SPC gs git status
SPC cf format SPC ca code action
SPC fc local config SPC ? active cheat sheet
Ctrl-hjkl windows SPC e sidebar
:ChopsticksStatus health :ChopsticksConfig preferences
```
Also test the boring path: save, quit, reopen Vim, edit over SSH, open a large
file, edit Markdown, and use a machine with missing optional tools.
## Exit criteria
- `s` as the default visible jump still feels worth the native override after
real editing.
- No high-frequency action requires remembering an undocumented key.
- Window/sidebar navigation feels faster than native `<C-w>` only.
- README, QUICKSTART, `:help chopsticks`, `SPC ?`, and `:ChopsticksTutor`
teach the same layout.
- No private wiki or external note is needed to remember the daily loop.
- `scripts/test.sh quick` and `scripts/test.sh vim` pass locally.
- The README GIF has been regenerated from `.github/demo.tape` after any public
key change.
- The release candidate has been tested on macOS and over SSH on Linux.
## Roll back
Return to the latest stable release:
```bash
cd ~/.vim
git fetch origin --tags
git checkout v2.2.0
vim -Nu ~/.vimrc -n -es +'PlugInstall --sync' +'qa!'
```
Or keep the code but switch back to the legacy layout:
```vim
let g:chopsticks_keymap_style = 'classic'
```

View file

@ -1,6 +1,63 @@
# Changelog
## Unreleased
## 2.3.0 — 2026-05-24
### Breaking
- Default keymap style is now `space`: `SPC` is the command leader and `,`
is reserved for filetype-local actions. Set
`let g:chopsticks_keymap_style = 'classic'` in
`${XDG_CONFIG_HOME:-~/.config}/chopsticks.vim` to keep the old comma layout
- In the default Space layout, Normal-mode `s` is now the fastest
EasyMotion over-window two-character jump. Use `cl` for native `s`
substitute and `cc` for native `S` substitute
- Git push and pull hotkeys are removed from both Space and classic layouts;
use `:Git push` / `:Git pull` explicitly for irreversible remote operations
### Added
- Canonical QWERTY/CapsLock-friendly Space leader layout:
`SPC SPC`, `SPC /`, `SPC ,`, `SPC w`, `SPC q`, `SPC rr`,
`SPC gs`, `SPC gl`, `SPC ca`, `SPC cr`, `SPC cf`, and `SPC ?`
- Native LSP motions in the default layout: `gd`, `gr`, `gI`, `gy`, `K`,
`[d`, and `]d`
- `:ChopsticksTutor` guided practice buffer for learning the final keymap
- `:ChopsticksCheatSheet` command, with `SPC ?` as the discoverable default
- `:ChopsticksHelp` and native `:help chopsticks` documentation for in-editor
support without a separate wiki
- `:ChopsticksConfig` and `:ChopsticksReload` for editing local preferences
without touching the managed `.vimrc`
- `:ChopsticksBeta`, `:ChopsticksBetaLog`, and `:ChopsticksBetaSession` for
guided release-candidate testing and local session notes
- Dedicated modules for buffers, utilities, files, runner, quickfix, status,
cheat sheet, tutor, release-candidate testing, and help
- Split test runner: `scripts/test.sh` now dispatches to
`scripts/test-quick.sh` and `scripts/test-vim.sh`
### Changed
- README, QUICKSTART, installer onboarding text, PR template, and demo tape now
teach the Space layout first while keeping the legacy classic layout documented
- `SPC ?` / `,?` cheat sheet output is generated from the active keymap style
and profile, so minimal installs no longer display disabled features
- Markdown actions now use localleader maps in the Space layout:
`,mp` preview and `,mt` table of contents
- `SPC U` opens UndoTree, `SPC z` toggles maximize, `SPC bp` / `SPC bn`
move between buffers, and quickfix/location-list actions live under `SPC x`
- `tools.vim` is now a compatibility placeholder; runtime behavior lives in
smaller focused modules
- CI/local smoke coverage now asserts Space defaults, classic compatibility,
missing push/pull hotkeys, tutor and cheat-sheet content, runner behavior,
large-file protection, and startup budget
### Fixed
- GitHub Actions now uses `actions/checkout@v5`, avoiding the Node.js 20
deprecation warning on PR checks
- `diffopt` enhancements now degrade safely on macOS system Vim builds that
report the required patch level but reject individual diff options
- Installer system-tool reporting now still detects already-installed tools on
Linux hosts where sudo is unavailable in non-interactive mode
## 2.2.0 — 2026-05-17

View file

@ -5,7 +5,7 @@
1. **No Node.js in the Vim runtime.** Plugins must work with pure VimScript — no coc.nvim or other Node-backed completion engines. External CLIs (prettier, eslint, markdownlint, stylelint, tsc) installed via npm are fine; ALE shells out to them as optional system tools, not as part of the Vim runtime.
2. **Startup matters.** Run `vim -u .vimrc -i NONE --startuptime /tmp/s.log -es -N -c qa!` before and after. If your change adds >1ms, it needs a good reason.
3. **Works on TTY.** Test over SSH. If it breaks in a terminal without true color, fix it or gate it behind `g:is_tty`.
4. **Native-first keymaps.** Enhance Vim's native behavior instead of replacing it. Do not override built-in motions, operators, text objects, or help-oriented keys for discoverability alone; prefer leader-prefixed or otherwise non-conflicting ergonomic mappings.
4. **Native-first keymaps.** Enhance Vim's native behavior instead of replacing it. Do not override built-in motions, operators, text objects, or help-oriented keys for discoverability alone. Rare exceptions, such as the default Space-layout `s` jump, must have a documented native replacement, cheat-sheet coverage, and a classic-layout fallback.
5. **One module, one concern.** Don't put git config in lsp.vim.
## Adding a plugin
@ -14,7 +14,7 @@
2. If it's not needed at startup, lazy-load it: `Plug 'foo/bar', { 'on': 'FooCommand' }`
3. Put config in the appropriate module
4. Check new mappings against native Vim behavior before adding them
5. Update the cheat sheet in `modules/tools.vim` if you add keybindings
5. Update the cheat sheet definitions in `modules/cheatsheet.vim` if you add keybindings
6. Run `scripts/test.sh vim` locally after installing plugins
7. Test on both macOS and Linux when changing terminal or package-manager behavior

View file

@ -1,9 +1,17 @@
# Quick Start
Five minutes from zero to a working Vim setup.
Five minutes to understand the chopsticks project loop.
This guide assumes you already know Vim's editing language. chopsticks keeps
that language intact and gives you one stable layer for the work around it:
jump on the visible screen, switch project files, grep, run, inspect code,
check git, and ask Vim which keys are active.
## Install
These commands install current `main`. For the 2.3.0 release-candidate
checklist and rollback steps, use [BETA.md](BETA.md).
```bash
curl -fsSL https://raw.githubusercontent.com/m1ngsama/chopsticks/main/get.sh | bash
curl -fsSL https://raw.githubusercontent.com/m1ngsama/chopsticks/main/get.sh | bash -s -- --profile=minimal
@ -18,50 +26,81 @@ without prompting. You can later put `let g:chopsticks_profile = 'minimal'` in
`${XDG_CONFIG_HOME:-~/.config}/chopsticks.vim` for a smaller core-only setup,
or use `full` for the heavier Markdown/LSP feedback.
The default keymap style is `space`: `SPC` is the command leader and `,` is
reserved for filetype-local actions. To use the legacy comma layout instead,
add this to `${XDG_CONFIG_HOME:-~/.config}/chopsticks.vim`:
```vim
let g:chopsticks_keymap_style = 'classic'
```
To switch later without reinstalling anything:
```bash
cd ~/.vim && ./install.sh --configure-only --profile=full
```
## Modes
## Daily loop
| Mode | Enter | Leave |
| ------ | --------------- | ------------- |
| Normal | startup default | — |
| Insert | `i` / `a` / `o` | `Esc` |
| Visual | `v` / `V` | `Esc` |
Train this first. It is the core reason to use chopsticks instead of assembling
the same pieces yourself:
```
SPC SPC open a project file
s + 2 chars jump to visible text
gd / gr / K inspect definition, references, docs
SPC rr run the current file
SPC / grep the project
SPC gs check git status
SPC ? show the active keymap
```
## Survival
```
Esc back to Normal
SPC w save
SPC q quit current window
:x / ZZ save + quit
:q! force quit
SPC ? cheat sheet (toggle sidebar)
SPC fc edit local preferences
:ChopsticksHelp full native help
```
Classic layout equivalents:
```
Esc back to Normal
,w save
,x save + quit
:q! force quit
,? cheat sheet (toggle sidebar)
,ec edit local preferences
:ChopsticksHelp full native help
```
## Find things
```
,ff fuzzy find file (git-aware)
,rg ripgrep project
,b search buffers
,fh recent files
,e file browser
,, last file
SPC SPC fuzzy find file (git-aware)
SPC / ripgrep project
SPC , search buffers
SPC fr recent files
SPC e sidebar at project cwd
SPC E sidebar at current file dir
SPC Tab last file
```
## Write code
```
,dd go to definition
,dk hover docs
,rn rename symbol
,ca code action
,f format
,cr run current file
gd go to definition
K hover docs
SPC cr rename symbol
SPC ca code action
SPC cf format
SPC rr run current file
Tab / S-Tab cycle completions
```
@ -70,31 +109,39 @@ Tab / S-Tab cycle completions
## Git
```
,gs status (s=stage, cc=commit)
,gd diff
,gb blame
,gp push
SPC gs status (s=stage, cc=commit)
SPC gd diff
SPC gb blame
SPC gl log graph
]x / [x conflict markers
```
## Edit
In the default Space layout, Normal-mode `s` is a fast visible-text jump.
Use `cl` when you want Vim's original single-character substitute behavior,
and `cc` when you want Vim's original line substitute behavior.
```
,S + 2 chars EasyMotion jump
s + 2 chars EasyMotion jump
SPC S + 2 chars same jump, discoverable fallback
cl / cc native s / S substitute replacements
gc toggle comment
cs"' change surrounding " to '
Alt+j / Alt+k move line
,u undo tree
,y clipboard yank
SPC U undo tree
SPC y clipboard yank
```
## Navigate
```
<C-w>h/j/k/l splits
,h / ,l prev / next buffer
,z maximize window
,tv / ,th terminal
Ctrl-h/j/k/l splits
<C-w>h/j/k/l native Vim fallback
SPC e, Ctrl-h/l open sidebar, enter/leave it
SPC bp / SPC bn prev / next buffer
SPC z maximize window
SPC tt / SPC th terminal
```
## Markdown
@ -110,10 +157,16 @@ syntax. Enable the heavier Markdown tools only when you want them.
## Health check
```
:ChopsticksHelp full native Vim help
:ChopsticksConfig edit local preferences
:ChopsticksReload reload after saving local preferences
:ChopsticksTutor guided practice for the final keymap
:ChopsticksStatus see what's installed and what's missing
```
The `,?` cheat sheet follows your active profile, so `minimal` users only see
The `SPC ?` cheat sheet follows your active profile, so `minimal` users only see
keys for features that are actually loaded.
See [README](README.md) for the full reference. See the [wiki](https://github.com/m1ngsama/chopsticks/wiki) for deep dives.
Inside Vim, `:help chopsticks` opens the same reference after helptags are
available. See [README](README.md) for the full reference. For release-candidate
testing and rollback, see [BETA.md](BETA.md).

200
README.md
View file

@ -1,11 +1,13 @@
<p align="center">
<img src=".github/demo.gif" alt="chopsticks demo" width="720">
<br>
<sub>For Vim users who already know how to edit: one trained project loop for jump, files, run, grep, git, and active key help.</sub>
</p>
<h1 align="center">chopsticks</h1>
<p align="center">
<strong>Vim for engineers. ~25 plugins, works over SSH.</strong>
<strong>A long-term Vim efficiency kit: find, jump, run, grep, git, LSP, and self-documenting keys over SSH.</strong>
</p>
<p align="center">
@ -18,19 +20,48 @@
---
Install current `main`:
```bash
curl -fsSL https://raw.githubusercontent.com/m1ngsama/chopsticks/main/get.sh | bash
```
Current `main` is preparing the 2.3.0 release. Use [BETA.md](BETA.md) for the
release-candidate checklist, rollback steps, and Space keymap test loop.
---
## Why
You SSH into a server. You need to edit code. You want LSP, fuzzy find, git integration, format-on-save — not a 20-minute setup.
chopsticks is for experienced Vim users who want one stable, ergonomic working
set they can train once and keep for years. It does not replace Vim's editing
language; it standardizes the project loop around it.
Stock Vim is a great editor core, but it does not ship that complete project
workflow. You still have to assemble fuzzy finding, project grep, git, LSP,
diagnostics, formatters, runners, terminal behavior, and a keymap that will not
collapse over SSH.
That assembly work is the pain chopsticks removes:
- **Project motion is scattered.** Files, buffers, grep, tags, marks, git, and
diagnostics live behind unrelated commands unless you design a system.
- **Plugin defaults fight muscle memory.** chopsticks gives QWERTY users one
canonical Space layout and keeps native Vim/LSP habits where they matter:
`gd`, `gr`, `K`, `Ctrl-h/j/k/l`, `<C-w>hjkl`, `cl`, `cc`.
- **Remote editing is fragile.** It is built to degrade on TTY, slow SSH, and
headless machines instead of assuming a GUI desktop.
- **Custom configs are hard to onboard.** `:ChopsticksHelp`, `SPC ?`,
`:ChopsticksTutor`, `:ChopsticksConfig`, and `:ChopsticksStatus` make the
active keymap, full help, local preferences, and missing tools visible inside
Vim.
You SSH into a server. You need to edit code. You want LSP, fuzzy find, git
integration, format-on-save — not a 20-minute setup.
chopsticks gives you a production-ready Vim config in one command. Pure VimScript — no Node.js for the core. Degrades gracefully on TTY. Works the same on your MacBook and your headless Arch box.
**2425 plugins** (tmux-navigator loads only inside tmux), LSP, linting, and a hand-built statusline. No bloat, no decorations, just tools.
**23+ plugins** depending on profile and opt-ins, LSP, linting, and a hand-built statusline. No bloat, no decorations, just tools.
## What's in the box
@ -39,14 +70,17 @@ chopsticks gives you a production-ready Vim config in one command. Pure VimScrip
| **LSP** | completion, go-to-def, hover, rename, code actions — pure VimScript ([vim-lsp](https://github.com/prabirshrestha/vim-lsp)) |
| **Lint + format** | [ALE](https://github.com/dense-analysis/ale) runs black, prettier, goimports, rustfmt on save |
| **Fuzzy find** | files, buffers, grep, tags, marks, commands — [FZF](https://github.com/junegunn/fzf.vim) |
| **Git** | status, diff, blame, push, pull, conflict markers — [fugitive](https://github.com/tpope/vim-fugitive) + [gitgutter](https://github.com/airblade/vim-gitgutter) |
| **Run file** | `,cr` — auto-detects Python, Go, Rust, JS, C, Shell, and more |
| **Git** | status, diff, blame, commit, log, conflict markers — [fugitive](https://github.com/tpope/vim-fugitive) + [gitgutter](https://github.com/airblade/vim-gitgutter) |
| **Run file** | `SPC rr` — auto-detects Python, Go, Rust, JS, C, Shell, and more |
| **Markdown** | quiet writing defaults, browser preview (`,mp`), table of contents (`,mt`) |
| **Diagnostics** | `:ChopsticksStatus` — see what's installed, what's missing, how to fix it |
| **TTY-aware** | degrades gracefully on SSH, console, slow links — never breaks |
## Install
These commands install current `main`. For the 2.3.0 release-candidate
checklist and rollback steps, use [BETA.md](BETA.md).
```bash
curl -fsSL https://raw.githubusercontent.com/m1ngsama/chopsticks/main/get.sh | bash
curl -fsSL https://raw.githubusercontent.com/m1ngsama/chopsticks/main/get.sh | bash -s -- --profile=minimal
@ -78,17 +112,20 @@ profile or uses `engineer`.
```vim
" Put this in ${XDG_CONFIG_HOME:-~/.config}/chopsticks.vim.
let g:chopsticks_profile = 'minimal' " core navigation/editing/git/markdown
let g:chopsticks_profile = 'engineer' " default: LSP, ALE, syntax extras
let g:chopsticks_profile = 'full' " engineer + heavier Markdown feedback
" let g:chopsticks_profile = 'minimal' " core navigation/editing/git/markdown
" let g:chopsticks_profile = 'full' " engineer + heavier Markdown feedback
let g:chopsticks_keymap_style = 'space' " default: Space leader grouped layout
" let g:chopsticks_keymap_style = 'classic' " optional legacy comma layout
let g:chopsticks_enable_jk_escape = 1 " optional: insert-mode jk exits insert
let g:chopsticks_enable_ctrl_s_save = 1 " optional: Ctrl-S saves
let g:chopsticks_enable_sudo_save_bang = 1 " optional: :w!! sudo save
let g:chopsticks_enable_completion_keymaps = 1 " optional: Tab/Enter completion
let g:chopsticks_enable_auto_pairs = 1 " optional: automatic pair insertion
let g:chopsticks_enable_terminal_keymaps = 1 " optional: terminal Esc/Ctrl navigation
let g:chopsticks_enable_tmux_navigator = 1 " optional: vim-tmux-navigator integration
let g:chopsticks_enable_exrc = 1 " optional: source project-local .vimrc/.exrc from CWD
let g:chopsticks_enable_reindent_file = 1 " optional: ,F reindents the entire file
let g:chopsticks_enable_reindent_file = 1 " optional: full-file reindent map
```
`minimal` avoids LSP, ALE, completion plugins, extra language syntax plugins,
@ -96,44 +133,73 @@ Startify, UndoTree, and browser Markdown preview. `full` keeps those and opts
into Markdown lint, format, spell, conceal, Marksman, and LSP virtual text.
Project updates leave `~/.config/chopsticks.vim` alone, so put local choices
there instead of editing the managed `.vimrc`. The `,?` cheat sheet follows the
active profile and only shows keys for enabled features.
there instead of editing the managed `.vimrc`. The `SPC ?` cheat sheet follows
the active profile and only shows keys for enabled features. Inside Vim, use
`:ChopsticksConfig` to edit that local file and `:ChopsticksReload` after
saving it.
## Keys
Leader: `,`
Default layout: `space`, leader `SPC`, localleader `,`.
This is the canonical layout for QWERTY keyboards with CapsLock mapped to
tap-Esc / hold-Ctrl. Escape and Ctrl stay at the system layer; Vim keeps the
native `<C-w>` window model as a fallback and standard LSP motions (`gd`,
`gr`, `K`).
Git push/pull are intentionally not bound to default hotkeys. Normal-mode `s`
is a screen-local EasyMotion jump; use `cl` for native `s` substitute and `cc`
for native `S`.
For learning the kit, use `:ChopsticksTutor` to train the core loop, `SPC ?`
for the active keymap, `:ChopsticksHelp` / `:help chopsticks` for full native
Vim help, `:ChopsticksConfig` for local preferences, and `:ChopsticksStatus`
for tool/LSP health.
`QUICKSTART.md` is the 5-minute path; this README is the full reference.
During release-candidate testing, `:ChopsticksBeta` opens the in-editor
checklist, `:ChopsticksBetaLog` opens editable local notes, and
`:ChopsticksBetaSession` appends a timestamped session block.
```
,ff fuzzy find file ,dd go to definition
,rg ripgrep project ,dk hover docs
,e toggle file sidebar ,cr run current file
,gs git status ,f format
,w save ,q quit
Esc exit insert mode ,? cheat sheet
SPC SPC fuzzy find file gd go to definition
SPC / ripgrep project K hover docs
SPC e toggle file sidebar SPC rr run current file
Ctrl-h/l enter/leave sidebar Ctrl-hjkl windows
SPC gs git status SPC cf format
SPC w save SPC q quit
Esc exit insert mode SPC ? cheat sheet
:ChopsticksConfig local prefs :ChopsticksReload reload
```
<details>
<summary><strong>All keybindings</strong></summary>
<summary><strong>Canonical Space keybindings</strong></summary>
### Fast Path
`SPC SPC` files | `SPC ,` buffers | `SPC /` grep | `SPC Tab` alternate buffer | `SPC e` browser | `SPC E` browser (file dir)
### Files
`,ff` find | `,b` buffers | `,rg` grep | `,rG` grep word | `,fh` recent | `,fl` lines | `,e` browser | `,E` browser (file dir) | `,,` last file
`SPC ff` files | `SPC fb` buffers | `SPC fg` git files | `SPC fr` recent | `SPC fl` buffer lines | `SPC fL` all lines | `SPC fc` local config | `SPC fv` edit vimrc | `SPC fV` reload
### Search
`SPC sg` grep | `SPC sw` grep word | `SPC s/` search history | `SPC s:` command history | `SPC sm` marks | `SPC st` tags | `SPC sr` replace word
### Code
`,dd` def | `,dt` type | `,di` impl | `,dr` refs | `,dk` docs | `,dp` `,dn` diagnostics | `[e` `]e` ALE errors | `,rn` rename | `,ca` action | `,o` outline | `,cr` run
`gd` def | `gr` refs | `gI` impl | `gy` type | `K` docs | `[d` `]d` LSP diagnostics | `[e` `]e` ALE errors | `SPC ca` action | `SPC cr` rename | `SPC cf` format | `SPC co` outline | `SPC ci` LSP status | `SPC rr` run
### Edit
`,S`+2ch jump | `gc` comment | `cs"'` surround | `Alt+j/k` move line | `,u` undo tree | `,y` clipboard | `,*` replace word | `,F` re-indent (v) | `,W` strip whitespace | `[<Space>` `]<Space>` blank lines
`s`+2ch jump | `SPC S` jump fallback | `cl` native `s` substitute | `cc` native `S` substitute | `gc` comment | `cs"'` surround | `Alt+j/k` move line | `SPC U` undo tree | `SPC y` clipboard | `SPC =` re-indent visual | `SPC cW` strip whitespace | `[<Space>` `]<Space>` blank lines
### Git
`,gs` status | `,gd` diff | `,gb` blame | `,gc` commit | `,gp` push | `,gl` pull | `,gL` log graph | `,gC` FZF commits | `,gB` buffer commits | `]x` `[x` conflict
`SPC gs` status | `SPC gd` diff | `SPC gb` blame | `SPC gc` commit | `SPC gl` log graph | `SPC gC` FZF commits | `SPC gB` buffer commits | `]x` `[x` conflict
### Windows
`<C-w>hjkl` navigate | `,z` maximize | `,h` `,l` buffers | `,bd` close buffer | `,=` `,` resize | `,tv` `,th` terminal
`Ctrl-h/j/k/l` windows | `<C-w>h/j/k/l` native fallback | `SPC z` maximize | `SPC bp` `SPC bn` buffers | `SPC bd` close buffer | `SPC bo` close other buffers | `SPC tt` `SPC th` terminal | `]q` `[q` quickfix | `SPC xq` `SPC xQ` open/close quickfix | `SPC xl` `SPC xL` open/close loclist
### Markdown
@ -141,11 +207,48 @@ Esc exit insert mode ,? cheat sheet
### Toggle
`F2` paste | `F3` line numbers | `F4` relative numbers | `F6` invisible chars | `SPC us` spell check | `SPC uf` format on save
### Survival
`SPC w` save | `SPC W` save all | `SPC q` quit | `:x` / `ZZ` save and quit | `SPC fc` local config | `SPC fV` reload | `SPC ?` cheat sheet | `:ChopsticksHelp` full help | `:ChopsticksTutor` practice | `:ChopsticksStatus` diagnostics
</details>
<details>
<summary><strong>Legacy classic keybindings</strong></summary>
### Classic Files
`,ff` find | `,b` buffers | `,rg` grep | `,rG` grep word | `,fh` recent | `,fl` lines | `,e` browser | `,E` browser (file dir) | `,,` last file
### Classic Code
`,dd` def | `,dt` type | `,di` impl | `,dr` refs | `,dk` docs | `,dp` `,dn` diagnostics | `[e` `]e` ALE errors | `,rn` rename | `,ca` action | `,o` outline | `,cr` run
### Classic Edit
`,S`+2ch jump | `gc` comment | `cs"'` surround | `Alt+j/k` move line | `,u` undo tree | `,y` clipboard | `,*` replace word | `,F` re-indent (v) | `,W` strip whitespace | `[<Space>` `]<Space>` blank lines
### Classic Git
`,gs` status | `,gd` diff | `,gb` blame | `,gc` commit | `,gL` log graph | `,gC` FZF commits | `,gB` buffer commits | `]x` `[x` conflict
### Classic Windows
`Ctrl-h/j/k/l` windows | `<C-w>h/j/k/l` native fallback | `,z` maximize | `,h` `,l` buffers | `,bd` close buffer | `,=` `,-` resize | `,tv` `,th` terminal
### Classic Markdown
`,mp` preview in browser | `,mt` table of contents
### Classic Toggle
`F2` paste | `F3` line numbers | `F4` relative numbers | `F6` invisible chars | `,ss` spell check | `,af` format on save
### Utilities
`,cp` copy full path | `,cf` copy filename | `,ev` edit vimrc | `,sv` reload vimrc | `,wa` save all | `:ChopsticksStatus` diagnostics
`,cp` copy full path | `,cf` copy filename | `,ec` local config | `,ev` edit vimrc | `,sv` reload | `,wa` save all | `:ChopsticksStatus` diagnostics
</details>
@ -192,18 +295,30 @@ For Markdown LSP, install or select `marksman` first.
```
~/.vim/
├── .vimrc thin loader
├── modules/
│ ├── env.vim TTY detection, truecolor, skip built-in plugins
│ ├── plugins.vim vim-plug + 2425 plugins
│ ├── core.vim settings, keymaps, performance
│ ├── ui.vim solarized, statusline, startify
│ ├── editing.vim easymotion, yank highlight, blank lines
│ ├── navigation.vim fzf, netrw sidebar, windows, terminal
│ ├── lsp.vim vim-lsp, asyncomplete
│ ├── lint.vim ale, format-on-save
│ ├── git.vim fugitive, gitgutter, conflict nav
│ ├── languages.vim vim-go, markdown, filetype settings
│ └── tools.vim run file, quickfix, cheat sheet, diagnostics
├── doc/
│ └── chopsticks.txt :help chopsticks
└── modules/
├── env.vim TTY detection, truecolor, skip built-in plugins
├── plugins.vim vim-plug + profile/option-driven plugins
├── core.vim settings, keymaps, performance
├── ui.vim solarized, statusline, startify
├── editing.vim easymotion, yank highlight, blank lines
├── navigation.vim fzf, netrw sidebar, windows, terminal
├── lsp.vim vim-lsp, asyncomplete
├── lint.vim ale, format-on-save
├── git.vim fugitive, gitgutter, conflict nav
├── languages.vim vim-go, markdown, filetype settings
├── buffers.vim buffer commands
├── utilities.vim config, reload, trim, clipboard helpers
├── files.vim auto mkdir, large-file protection
├── runner.vim run current file
├── quickfix.vim quickfix and location-list helpers
├── status.vim :ChopsticksStatus diagnostics
├── cheatsheet.vim SPC ? and :ChopsticksCheatSheet
├── tutor.vim :ChopsticksTutor guided practice
├── beta.vim :ChopsticksBeta release checklist
├── help.vim :ChopsticksHelp native Vim help
└── tools.vim compatibility placeholder
```
Each module is self-contained. Comment out one line in `.vimrc` to disable it. Add your own with `call s:load('mine')`.
@ -228,12 +343,23 @@ Each module is self-contained. Comment out one line in `.vimrc` to disable it. A
| Everything slow | Large file? Auto-disabled >10MB |
| What's installed? | `:ChopsticksStatus` shows tools, LSP, linters |
More in the [wiki](https://github.com/m1ngsama/chopsticks/wiki).
For deeper checks, start with `:ChopsticksStatus`, `SPC ?`,
`:ChopsticksTutor`, `:ChopsticksHelp`, `:ChopsticksConfig`, and
[QUICKSTART.md](QUICKSTART.md).
## Contributing
See [CONTRIBUTING.md](CONTRIBUTING.md). The two rules that matter: no Node.js in the Vim runtime, and don't regress startup time.
Regenerate the README demo after changing public keybindings:
```bash
vhs .github/demo.tape
```
The tape uses `.github/demo-project` and forces the current repository `.vimrc`,
so the GIF should show the same keymap the code actually ships.
## License
[MIT](LICENSE)

181
doc/chopsticks.txt Normal file
View file

@ -0,0 +1,181 @@
*chopsticks.txt* *chopsticks* A long-term Vim efficiency kit
==============================================================================
CONTENTS *chopsticks-contents*
1. What chopsticks solves...............|chopsticks-why|
2. First five minutes...................|chopsticks-start|
3. Canonical Space layout...............|chopsticks-space|
4. Commands.............................|chopsticks-commands|
5. Profiles and local config............|chopsticks-profiles|
6. Release testing......................|chopsticks-release-test|
7. Troubleshooting......................|chopsticks-troubleshooting|
==============================================================================
WHAT CHOPSTICKS SOLVES *chopsticks-why*
Chopsticks is for experienced Vim users who want one stable, ergonomic working
set they can train once and keep for years. It connects fuzzy find, project
grep, git, LSP, linting, formatting, runners, quickfix navigation, and
self-documenting keys for people who edit locally and over SSH.
It is meant to supplement stock Vim, not replace Vim muscle memory. Native
motions and well-known conventions stay where they are useful: gd, gr, K,
Ctrl-h/j/k/l, <C-w>hjkl, cl, cc, quickfix, and normal Vim commands.
==============================================================================
FIRST FIVE MINUTES *chopsticks-start*
Inside Vim:
>
:ChopsticksTutor guided practice page
SPC ? active keymap cheat sheet
:ChopsticksStatus tool, plugin, and LSP diagnostics
:ChopsticksHelp this help page
:ChopsticksConfig edit local preferences
<
Daily loop:
>
SPC SPC find file SPC / grep project
s + 2ch jump on screen gd / gr definition / references
K hover docs SPC rr run current file
SPC gs git status SPC cf format
SPC w save SPC q quit
<
==============================================================================
CANONICAL SPACE LAYOUT *chopsticks-space*
Default layout: Space leader, comma localleader.
This layout assumes a QWERTY keyboard and CapsLock mapped at the system layer
to tap-Esc / hold-Ctrl. Ctrl-h/j/k/l is the fast path for Vim windows;
<C-w>hjkl remains the native fallback. Standard LSP motions stay on code.
Normal-mode s is a visible EasyMotion jump. This is intentionally different
from stock Vim because screen-local jumping is higher value in project editing.
Use cl for native s substitute and cc for native S substitute.
High-frequency keys:
>
SPC SPC files SPC , buffers
SPC / grep project SPC Tab alternate file
SPC e/E file sidebar SPC rr run file
Ctrl-hjkl windows Ctrl-h/l enter/leave sidebar
SPC gs git status SPC gl git log graph
SPC ca code action SPC cr rename
SPC cf format SPC fc edit local config
SPC fV reload config SPC ? cheat sheet
<
Classic comma mappings remain available:
>
let g:chopsticks_keymap_style = 'classic'
<
==============================================================================
COMMANDS *chopsticks-commands*
User-facing commands:
>
:ChopsticksHelp open this help
:ChopsticksConfig edit local preferences
:ChopsticksReload reload chopsticks after config changes
:ChopsticksTutor guided practice
:ChopsticksCheatSheet active keymap reference
:ChopsticksStatus health diagnostics
:ChopsticksBeta release checklist
:ChopsticksBetaLog editable release notes
:ChopsticksBetaSession append a timestamped release note block
<
LSP commands come from vim-lsp:
>
:LspInstallServer install a server for the current filetype
:LspStatus inspect attached servers
<
==============================================================================
PROFILES AND LOCAL CONFIG *chopsticks-profiles*
Keep personal choices outside the managed .vimrc:
>
" ${XDG_CONFIG_HOME:-~/.config}/chopsticks.vim
let g:chopsticks_profile = 'engineer'
let g:chopsticks_keymap_style = 'space'
<
Open that file from inside Vim:
>
:ChopsticksConfig
<
Reload after saving it:
>
:ChopsticksReload
<
Profiles:
minimal Core navigation, editing, git, Markdown. No LSP/ALE/completion.
engineer Default. LSP, ALE, completion, syntax extras.
full Engineer plus heavier Markdown feedback.
Optional habits:
>
let g:chopsticks_enable_jk_escape = 1
let g:chopsticks_enable_ctrl_s_save = 1
let g:chopsticks_enable_auto_pairs = 1
let g:chopsticks_enable_terminal_keymaps = 1
<
==============================================================================
RELEASE TESTING *chopsticks-release-test*
For the 2.3.0 release candidate, record real editing friction instead of
abstract opinions:
>
:ChopsticksBeta compact release checklist
:ChopsticksBetaLog open editable local notes
:ChopsticksBetaSession append a new session block
<
Default release log:
>
${XDG_CONFIG_HOME:-~/.config}/chopsticks-2.3.0.md
<
Exit criteria before a stable release:
- s as jump still feels worth the native override.
- No high-frequency action needs an undocumented key.
- README, QUICKSTART, :help chopsticks, SPC ?, and :ChopsticksTutor agree.
- quick and Vim smoke tests pass locally and over SSH.
- The README GIF matches the public keymap.
==============================================================================
TROUBLESHOOTING *chopsticks-troubleshooting*
Start with:
>
:ChopsticksStatus
SPC ?
:ChopsticksTutor
:ChopsticksHelp
:ChopsticksConfig
<
Common fixes:
Plugins missing :PlugInstall
LSP missing Open that filetype, then :LspInstallServer
Need active keys SPC ?
Need full docs :help chopsticks
Need local config :ChopsticksConfig
Changed local config :ChopsticksReload
Need release notes :ChopsticksBetaSession
Slow large file Syntax, undo, swap, and ALE are auto-reduced
==============================================================================
vim:tw=78:ts=8:ft=help:norl:

View file

@ -719,6 +719,21 @@ if [[ $_plug_count -eq 0 ]]; then
fi
ok "Plugins installed ($_plug_count)"
step "Installing Vim help"
if [[ -d "$SCRIPT_DIR/doc" ]]; then
# shellcheck disable=SC2016 # $CHOPSTICKS_HELP_DIR is expanded by Vim.
if CHOPSTICKS_HELP_DIR="$SCRIPT_DIR/doc" \
"$VIM_BIN" -Nu NONE -i NONE -n -es -N \
-c 'execute "silent! helptags " . fnameescape($CHOPSTICKS_HELP_DIR)' \
-c 'qa!' >/dev/null 2>&1; then
ok ":help chopsticks"
else
warn "Could not generate Vim help tags. Inside Vim, run: :ChopsticksHelp"
fi
else
skip "Vim help docs not found"
fi
# ============================================================================
# 4. Module Selection
# ============================================================================
@ -741,22 +756,22 @@ _I_NPM=-1; _I_PYTHON=-1; _I_GO=-1; _I_TMUX=-1
# Is any package manager available?
HAS_PKG_MGR=0
if [[ $HAS_BREW -eq 1 ]] || \
{ [[ $HAS_APT -eq 1 || $HAS_PACMAN -eq 1 || $HAS_DNF -eq 1 ]] && [[ $HAS_SUDO -eq 1 ]]; }; then
[[ $HAS_APT -eq 1 || $HAS_PACMAN -eq 1 || $HAS_DNF -eq 1 ]]; then
HAS_PKG_MGR=1
fi
# ── System tools ─────────────────────────────────────────────────────────────
if [[ $HAS_PKG_MGR -eq 1 ]]; then
_I_RIPGREP=$_idx
_ITEMS+=("ripgrep|,rg / ,rG project-wide search · powers FZF preview|1")
_ITEMS+=("ripgrep|SPC / project search · SPC sw word search · powers FZF preview|1")
: $(( _idx++ ))
_I_FZF=$_idx
_ITEMS+=("fzf|,ff fuzzy file search · ,b buffers · ,rt tag search|1")
_ITEMS+=("fzf|SPC SPC files · SPC , buffers · SPC st tag search|1")
: $(( _idx++ ))
_I_CTAGS=$_idx
_ITEMS+=("universal-ctags|Optional symbol index for ,rt tag jumps|0")
_ITEMS+=("universal-ctags|Optional symbol index for SPC st tag jumps|0")
: $(( _idx++ ))
if [[ $_PROFILE_TOOLING -eq 1 ]]; then
@ -848,6 +863,11 @@ _do_sys() {
if command -v "$check" >/dev/null 2>&1; then
ok "$name (already installed)"; return
fi
if [[ $OS != "macos" && $HAS_SUDO -ne 1 ]]; then
skip "$name — sudo not available, install manually"
SKIPPED+=("$name")
return
fi
if pkg_install "$brew_p" "$apt_p" "$pac_p" "$dnf_p"; then
ok "$name"; INSTALLED+=("$name")
else
@ -1201,9 +1221,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}SPC ?${NC} Open cheat sheet — your map of every keybinding"
echo -e " ${CYAN}Esc${NC} Exit insert mode → back to Normal"
echo -e " ${CYAN},x${NC} Save and quit"
echo -e " ${CYAN}SPC q${NC} Quit current window"
echo -e " ${CYAN}:x${NC} / ${CYAN}ZZ${NC} Save and quit"
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"

124
modules/beta.vim Normal file
View file

@ -0,0 +1,124 @@
" beta.vim — in-editor release-candidate checklist
let g:chopsticks_beta_label = get(g:, 'chopsticks_beta_label', '2.3.0')
function! s:OpenBetaGuide() abort
let l:name = '__ChopsticksBeta__'
if bufwinnr(l:name) > 0
execute bufwinnr(l:name) . 'wincmd w | bd'
return
endif
execute 'botright new ' . l:name
resize 34
setlocal buftype=nofile bufhidden=wipe nobuflisted noswapfile
setlocal nowrap nonumber norelativenumber signcolumn=no
let l:lines = [
\ ' chopsticks 2.3.0 q close',
\ ' ─────────────────────────────',
\ '',
\ ' goal',
\ ' Prove this can be the long-term project loop.',
\ ' Record real editing friction before release.',
\ '',
\ ' daily loop',
\ ' SPC SPC find file',
\ ' s + 2ch jump on screen',
\ ' gd / gr definition / references',
\ ' K hover docs',
\ ' SPC / grep project',
\ ' SPC rr run current file',
\ ' SPC gs git status',
\ ' SPC cf format',
\ ' SPC ? active cheat sheet',
\ ' :ChopsticksBetaSession new note block',
\ '',
\ ' record',
\ ' task: project navigation, code, grep, git, LSP, Markdown, SSH',
\ ' first key tried when stuck',
\ ' whether SPC ?, :ChopsticksTutor, or :ChopsticksStatus answered it',
\ ' any key that felt slow, awkward, surprising, or easy to mistype',
\ '',
\ ' exit criteria',
\ ' s as jump still feels worth the native override',
\ ' no high-frequency action needs an undocumented key',
\ ' window/sidebar navigation beats native <C-w> only',
\ ' README, QUICKSTART, SPC ?, and tutor teach the same layout',
\ ' no private wiki is needed to remember the daily loop',
\ ' quick/vim tests pass locally and over SSH',
\ '',
\ ' files',
\ ' BETA.md release checklist and rollback',
\ ' :ChopsticksBetaLog editable local release notes',
\ ' :ChopsticksBetaSession append a new session block',
\ ' QUICKSTART.md five-minute path',
\ ' README.md complete reference',
\ ]
call setline(1, l:lines)
setlocal nomodifiable readonly
nnoremap <buffer> <silent> q :bd<CR>
nnoremap <buffer> <silent> ? :ChopsticksCheatSheet<CR>
endfunction
function! s:BetaLogPath() abort
let l:configured = get(g:, 'chopsticks_beta_log', '')
if !empty(l:configured)
return expand(l:configured)
endif
let l:xdg = !empty($XDG_CONFIG_HOME) && $XDG_CONFIG_HOME =~# '^/'
\ ? $XDG_CONFIG_HOME
\ : '~/.config'
return expand(l:xdg . '/chopsticks-2.3.0.md')
endfunction
function! s:SessionBlock() abort
return [
\ '',
\ '## ' . strftime('%Y-%m-%d %H:%M'),
\ '',
\ '- Task:',
\ '- First key tried when stuck:',
\ '- Did SPC ?, :ChopsticksTutor, or :ChopsticksStatus answer it:',
\ '- Friction:',
\ '- Decision:',
\ ]
endfunction
function! s:EnsureBetaLog(path) abort
let l:path = a:path
let l:dir = fnamemodify(l:path, ':h')
if !isdirectory(l:dir)
call mkdir(l:dir, 'p')
endif
if !filereadable(l:path)
call writefile([
\ '# chopsticks 2.3.0 release log',
\ '',
\ 'Use :ChopsticksBeta for the release checklist. Keep one session block per real editing session.',
\ ] + s:SessionBlock(), l:path)
endif
endfunction
function! s:OpenBetaLog() abort
let l:path = s:BetaLogPath()
call s:EnsureBetaLog(l:path)
execute 'edit ' . fnameescape(l:path)
setlocal filetype=markdown
endfunction
function! s:AppendBetaSession() abort
let l:path = s:BetaLogPath()
call s:EnsureBetaLog(l:path)
call writefile(s:SessionBlock(), l:path, 'a')
execute 'edit ' . fnameescape(l:path)
setlocal filetype=markdown
normal! G
endfunction
command! ChopsticksBeta call s:OpenBetaGuide()
command! ChopsticksBetaLog call s:OpenBetaLog()
command! ChopsticksBetaSession call s:AppendBetaSession()

18
modules/buffers.vim Normal file
View file

@ -0,0 +1,18 @@
" buffers.vim — buffer commands
command! Bclose call <SID>BufcloseCloseIt()
function! <SID>BufcloseCloseIt()
let l:currentBufNum = bufnr("%")
let l:alternateBufNum = bufnr("#")
if buflisted(l:alternateBufNum)
buffer #
else
bnext
endif
if bufnr("%") == l:currentBufNum
new
endif
if buflisted(l:currentBufNum)
execute("bdelete! " . l:currentBufNum)
endif
endfunction

291
modules/cheatsheet.vim Normal file
View file

@ -0,0 +1,291 @@
" cheatsheet.vim — active keymap reference
function! s:OpenCheatSheet(lines) abort
let l:name = '__ChopsticksCheatSheet__'
if bufwinnr(l:name) > 0
execute bufwinnr(l:name) . 'wincmd w | bd'
return
endif
execute 'vertical botright new ' . l:name
vertical resize 42
setlocal buftype=nofile bufhidden=wipe nobuflisted noswapfile
setlocal nowrap nonumber norelativenumber signcolumn=no
setlocal winfixwidth
call setline(1, a:lines)
setlocal nomodifiable readonly
nnoremap <buffer> <silent> q :bd<CR>
nnoremap <buffer> <silent> <leader>? :bd<CR>
endfunction
function! s:CheatSheet() abort
let l:has_lsp = get(g:, 'chopsticks_enable_lsp', 1)
let l:has_lint = get(g:, 'chopsticks_enable_lint', 1)
let l:has_undotree = exists('g:plugs["undotree"]')
let l:has_previm = exists('g:plugs["previm"]')
if g:chopsticks_space_keymaps
let l:lines = [
\ ' chopsticks <Space>? close',
\ ' ─────────────────────────────────',
\ '',
\ ' trained loop:',
\ ' files → s jump → gd/K',
\ ' run → grep → git',
\ '',
\ ' ── fast path ─────────────',
\ ' SPC SPC files',
\ ' SPC , buffers',
\ ' SPC / grep project',
\ ' SPC Tab last file',
\ ' SPC e sidebar (cwd)',
\ ' SPC E sidebar (file dir)',
\ '',
\ ' ── files/find ────────────',
\ ' SPC ff files',
\ ' SPC fb buffers',
\ ' SPC fg git files',
\ ' SPC fr recent files',
\ ' SPC fl lines in buffer',
\ ' SPC sc commands',
\ ' SPC sm marks',
\ ' SPC s/ search history',
\ ' SPC s: command history',
\ ' SPC sg grep project',
\ ' SPC sw grep word',
\ ' SPC st tags',
\ '',
\ ' ── code ──────────────────',
\ ]
if l:has_lsp
call extend(l:lines, [
\ ' gd definition',
\ ' gr references',
\ ' gI implementation',
\ ' gy type definition',
\ ' K hover docs',
\ ' [d ]d LSP diagnostics',
\ ' SPC ca code action',
\ ' SPC cr rename',
\ ' SPC cf format',
\ ' SPC ci LSP status',
\ ' SPC co outline',
\ ' SPC cS workspace symbols',
\ ' :LspInstallServer setup LSP',
\ ' :ChopsticksStatus check LSP setup',
\ ])
endif
call extend(l:lines, [
\ ' SPC rr run file',
\ ' SPC cW strip trailing',
\ ' SPC c= re-indent file (opt-in)',
\ ' SPC = re-indent (v)',
\ ])
if l:has_previm
call add(l:lines, ' ,mp markdown preview')
endif
call add(l:lines, ' ,mt table of contents')
if l:has_lint
call extend(l:lines, [
\ ' [e ]e ALE errors',
\ ' SPC xd ALE detail',
\ ' SPC uf format on save',
\ ])
endif
call extend(l:lines, [
\ '',
\ ' ── edit ──────────────────',
\ ' s+2ch easymotion jump',
\ ' gc comment',
\ ' cl / cc native s / S substitute',
\ ' SPC S+2ch jump fallback',
\ ' cs"'' surround',
\ ])
if l:has_undotree
call add(l:lines, ' SPC U undo tree')
endif
call extend(l:lines, [
\ ' SPC y/p clipboard y/p (v)',
\ ' Alt+j/k move line (v)',
\ ' SPC sr replace word (v)',
\ '',
\ ' ── git ───────────────────',
\ ' SPC gs status',
\ ' SPC gd diff',
\ ' SPC gb blame',
\ ' SPC gc commit',
\ ' SPC gl log graph',
\ ' SPC gC FZF commits',
\ ' SPC gB FZF buffer commits',
\ ' [x ]x conflict markers',
\ '',
\ ' ── windows ───────────────',
\ ' Ctrl-hjkl windows',
\ ' <C-w>hjkl native fallback',
\ ' SPC bp/bn prev / next buf',
\ ' SPC bd close buffer',
\ ' SPC bo close other buffers',
\ ' SPC z maximize toggle',
\ ' SPC tt/th terminal / split',
\ ' ]q [q next / prev qf',
\ ' SPC xq/xQ open / close qf',
\ ' SPC xl/xL open / close loclist',
\ '',
\ ' ── toggle ────────────────',
\ ' F2 paste mode',
\ ' F3 line numbers',
\ ' F4 relative numbers',
\ ' F6 invisible chars',
\ ' SPC us spell check',
\ '',
\ ' ── survival ──────────────',
\ ' SPC w save',
\ ' SPC W save all',
\ ' SPC q quit',
\ ' :x / ZZ save + quit',
\ ' Esc exit insert',
\ ' SPC fc edit local config',
\ ' SPC fv edit vimrc',
\ ' SPC fV reload vimrc',
\ ' :ChopsticksHelp full help',
\ ' :ChopsticksConfig local config',
\ ' :ChopsticksReload reload config',
\ ' :ChopsticksTutor practice',
\ ' :ChopsticksStatus health',
\ ' :ChopsticksBeta release checklist',
\ ' :ChopsticksBetaLog release notes',
\ ' :ChopsticksBetaSession new release note',
\ ])
call s:OpenCheatSheet(l:lines)
return
endif
let l:lines = [
\ ' chopsticks ,? close',
\ ' ─────────────────────────────',
\ '',
\ ' trained loop:',
\ ' files → jump → inspect',
\ ' run → grep → git',
\ '',
\ ' ── files ──────────────────',
\ ' ,ff files',
\ ' ,b buffers',
\ ' ,rg grep project',
\ ' ,rG grep word',
\ ' ,e sidebar (cwd)',
\ ' ,E sidebar (file dir)',
\ ' ,, last file',
\ ' ,fh recent files',
\ ' ,fl lines in buffer',
\ ' ,fc commands',
\ ' ,fm marks',
\ '',
\ ' ── code ──────────────────',
\ ]
if l:has_lsp
call extend(l:lines, [
\ ' ,dd definition',
\ ' ,dt type definition',
\ ' ,di implementation',
\ ' ,dr references',
\ ' ,dk hover docs',
\ ' ,rn rename',
\ ' ,ca code action',
\ ' ,f format',
\ ' ,o outline',
\ ' ,dp ,dn LSP diagnostics',
\ ' :LspInstallServer setup LSP',
\ ' :ChopsticksStatus check LSP setup',
\ ])
endif
call add(l:lines, ' ,cr run file')
if l:has_previm
call add(l:lines, ' ,mp markdown preview')
endif
call add(l:lines, ' ,mt table of contents')
if l:has_lint
call extend(l:lines, [
\ ' [e ]e ALE errors',
\ ' ,af format on save',
\ ])
endif
call extend(l:lines, [
\ '',
\ ' ── edit ──────────────────',
\ ' gc comment',
\ ' ,S+2ch easymotion jump',
\ ' cs"'' surround',
\ ])
if l:has_undotree
call add(l:lines, ' ,u undo tree')
endif
call extend(l:lines, [
\ ' ,y ,p clipboard y/p (v)',
\ ' Alt+j/k move line (v)',
\ ' ,* replace word (v)',
\ ' ,F re-indent (v)',
\ ' ,W strip trailing',
\ '',
\ ' ── git ───────────────────',
\ ' ,gs status',
\ ' ,gd diff',
\ ' ,gb blame',
\ ' ,gc commit',
\ ' ,gL log graph',
\ ' ,gC FZF commits',
\ ' [x ]x conflict markers',
\ '',
\ ' ── windows ───────────────',
\ ' Ctrl-hjkl windows',
\ ' <C-w>hjkl native fallback',
\ ' ,h ,l prev / next buf',
\ ' ,bd close buffer',
\ ' ,z maximize toggle',
\ ' ,= ,- resize height',
\ ' ,tv ,th terminal v / h',
\ ' ]q [q next / prev qf',
\ ' ,qo ,qc open / close qf',
\ '',
\ ' ── toggle ────────────────',
\ ' F2 paste mode',
\ ' F3 line numbers',
\ ' F4 relative numbers',
\ ' F6 invisible chars',
\ ' ,ss spell check',
\ '',
\ ' ── survival ──────────────',
\ ' ,w save',
\ ' ,q quit',
\ ' ,x save + quit',
\ ' Esc exit insert',
\ ' ,ec edit local config',
\ ' ,ev edit vimrc',
\ ' ,sv reload vimrc',
\ ' :ChopsticksHelp full help',
\ ' :ChopsticksConfig local config',
\ ' :ChopsticksReload reload config',
\ ' :ChopsticksTutor practice',
\ ' :ChopsticksStatus health',
\ ' :ChopsticksBeta release checklist',
\ ' :ChopsticksBetaLog release notes',
\ ' :ChopsticksBetaSession new release note',
\ ])
call s:OpenCheatSheet(l:lines)
endfunction
command! ChopsticksCheatSheet call s:CheatSheet()
nnoremap <silent> <leader>? :ChopsticksCheatSheet<CR>

View file

@ -94,22 +94,43 @@ set smartindent
" ── Leader ──────────────────────────────────────────────────────────────────
let mapleader = ","
if g:chopsticks_space_keymaps
let mapleader = "\<Space>"
let maplocalleader = ","
else
let mapleader = ","
endif
" ── Basic Keymaps ───────────────────────────────────────────────────────────
nnoremap <leader>w :w<cr>
nnoremap <leader>q :q<cr>
nnoremap <leader>x :x<cr>
if g:chopsticks_space_keymaps
nnoremap <leader>w :w<cr>
nnoremap <leader>W :wa<cr>
nnoremap <leader>q :q<cr>
nnoremap <silent> <leader><cr> :noh<cr>
nnoremap <silent> <leader>uh :noh<cr>
nnoremap <leader>bd :Bclose<cr>
nnoremap <leader>ba :bufdo bd<cr>
nnoremap <leader>l :bnext<cr>
nnoremap <leader>h :bprevious<cr>
nnoremap <leader>bd :Bclose<cr>
nnoremap <leader>ba :bufdo bd<cr>
nnoremap <leader>bo :%bd<bar>e#<bar>bd#<cr>
nnoremap <leader>bn :bnext<cr>
nnoremap <leader>bp :bprevious<cr>
nnoremap <leader>cd :lcd %:p:h<cr>:pwd<cr>
nnoremap <leader>fd :lcd %:p:h<cr>:pwd<cr>
else
nnoremap <leader>w :w<cr>
nnoremap <leader>q :q<cr>
nnoremap <leader>x :x<cr>
nnoremap <silent> <leader><cr> :noh<cr>
nnoremap <leader>bd :Bclose<cr>
nnoremap <leader>ba :bufdo bd<cr>
nnoremap <leader>l :bnext<cr>
nnoremap <leader>h :bprevious<cr>
nnoremap <leader>cd :lcd %:p:h<cr>:pwd<cr>
endif
nnoremap <leader>v `[v`]
@ -118,7 +139,11 @@ nnoremap <M-k> :m .-2<CR>==
vnoremap <M-j> :m '>+1<CR>gv=gv
vnoremap <M-k> :m '<-2<CR>gv=gv
nnoremap <silent> <leader>ss :setlocal spell!<CR>:echo 'Spell: ' . (&spell ? 'ON' : 'OFF')<CR>
if g:chopsticks_space_keymaps
nnoremap <silent> <leader>us :setlocal spell!<CR>:echo 'Spell: ' . (&spell ? 'ON' : 'OFF')<CR>
else
nnoremap <silent> <leader>ss :setlocal spell!<CR>:echo 'Spell: ' . (&spell ? 'ON' : 'OFF')<CR>
endif
nnoremap <silent> <F2> :set paste!<CR>:echo 'Paste: ' . (&paste ? 'ON' : 'OFF')<CR>
nnoremap <silent> <F3> :set invnumber<CR>:echo 'Line numbers: ' . (&number ? 'ON' : 'OFF')<CR>
@ -157,8 +182,15 @@ if has('clipboard')
vnoremap <leader>P "+P
endif
nnoremap <leader>qo :copen<CR>
nnoremap <leader>qc :cclose<CR>
if g:chopsticks_space_keymaps
nnoremap <leader>xq :copen<CR>
nnoremap <leader>xQ :cclose<CR>
nnoremap <leader>xl :lopen<CR>
nnoremap <leader>xL :lclose<CR>
else
nnoremap <leader>qo :copen<CR>
nnoremap <leader>qc :cclose<CR>
endif
augroup ChopstickResize
autocmd!
@ -193,7 +225,10 @@ endif
set sessionoptions=blank,buffers,curdir,folds,help,tabpages,winsize,winpos,terminal
if has("patch-8.1.0360")
set diffopt=filler,internal,context:3,algorithm:histogram,indent-heuristic
silent! set diffopt+=internal
silent! set diffopt+=context:3
silent! set diffopt+=algorithm:histogram
silent! set diffopt+=indent-heuristic
endif
" ── Format Options ──────────────────────────────────────────────────────────

View file

@ -6,16 +6,27 @@ let g:EasyMotion_do_mapping = 0
let g:EasyMotion_smartcase = 1
if exists('g:plugs["vim-easymotion"]')
nmap <Leader>S <Plug>(easymotion-overwin-f2)
nmap <Leader>j <Plug>(easymotion-j)
nmap <Leader>k <Plug>(easymotion-k)
if g:chopsticks_space_keymaps
" In the canonical layout, cl/cc cover native s/S substitute behavior;
" s becomes the fastest screen-local jump entry.
nmap s <Plug>(easymotion-overwin-f2)
nmap <Leader>S <Plug>(easymotion-overwin-f2)
else
nmap <Leader>S <Plug>(easymotion-overwin-f2)
nmap <Leader>j <Plug>(easymotion-j)
nmap <Leader>k <Plug>(easymotion-k)
endif
endif
" ── UndoTree ────────────────────────────────────────────────────────────────
if exists('g:plugs["undotree"]')
nnoremap <F5> :UndotreeToggle<CR>
nnoremap <leader>u :UndotreeToggle<CR>
if g:chopsticks_space_keymaps
nnoremap <leader>U :UndotreeToggle<CR>
else
nnoremap <leader>u :UndotreeToggle<CR>
endif
endif
" ── Yank Highlight ──────────────────────────────────────────────────────────

View file

@ -11,6 +11,12 @@ if index(['minimal', 'engineer', 'full'], g:chopsticks_profile) < 0
let g:chopsticks_profile = 'engineer'
endif
let g:chopsticks_keymap_style = get(g:, 'chopsticks_keymap_style', 'space')
if index(['classic', 'space'], g:chopsticks_keymap_style) < 0
let g:chopsticks_keymap_style = 'space'
endif
let g:chopsticks_space_keymaps = g:chopsticks_keymap_style ==# 'space'
let s:profile_full = g:chopsticks_profile ==# 'full'
let s:profile_minimal = g:chopsticks_profile ==# 'minimal'
@ -28,6 +34,8 @@ let g:chopsticks_enable_auto_pairs = get(g:,
\ 'chopsticks_enable_auto_pairs', 0)
let g:chopsticks_enable_terminal_keymaps = get(g:,
\ 'chopsticks_enable_terminal_keymaps', 0)
let g:chopsticks_enable_tmux_navigator = get(g:,
\ 'chopsticks_enable_tmux_navigator', 0)
let g:chopsticks_markdown_lint = get(g:, 'chopsticks_markdown_lint',
\ s:profile_full)

54
modules/files.vim Normal file
View file

@ -0,0 +1,54 @@
" files.vim — file safety and large-file handling
function! s:MkNonExDir(file, buf)
if empty(getbufvar(a:buf, '&buftype')) && a:file !~# '\v^\w+\:\/'
let dir = fnamemodify(a:file, ':h')
if !isdirectory(dir)
call mkdir(dir, 'p')
endif
endif
endfunction
augroup BWCCreateDir
autocmd!
autocmd BufWritePre *
\ if !empty(expand('<afile>')) |
\ call s:MkNonExDir(expand('<afile>'), +expand('<abuf>')) |
\ endif
augroup END
let g:LargeFile = get(g:, 'LargeFile', 1024 * 1024 * 10)
let s:tty_large = g:is_tty ? 512000 : g:LargeFile
function! s:ApplyLargeFileSettings() abort
if get(b:, 'chopsticks_large_file', 0)
setlocal bufhidden=unload undolevels=-1 noswapfile
let b:ale_enabled = 0
if &l:syntax !=# ''
setlocal syntax=
endif
elseif get(b:, 'chopsticks_tty_large_file', 0)
if &l:syntax !=# ''
setlocal syntax=
endif
endif
endfunction
function! s:MarkLargeFile(file) abort
if empty(a:file)
return
endif
let l:fsize = getfsize(a:file)
if l:fsize > g:LargeFile || l:fsize == -2
let b:chopsticks_large_file = 1
elseif g:is_tty && l:fsize > s:tty_large
let b:chopsticks_tty_large_file = 1
endif
call s:ApplyLargeFileSettings()
endfunction
augroup ChopstickLargeFile
autocmd!
autocmd BufReadPre * call s:MarkLargeFile(expand('<afile>'))
autocmd BufReadPost,FileType,Syntax * call s:ApplyLargeFileSettings()
augroup END

View file

@ -14,11 +14,13 @@ let g:gitgutter_sign_modified_removed = '~'
if exists('g:plugs["vim-fugitive"]')
nnoremap <leader>gs :Git status<CR>
nnoremap <leader>gc :Git commit<CR>
nnoremap <leader>gp :Git push<CR>
nnoremap <leader>gl :Git pull<CR>
nnoremap <leader>gd :Gdiffsplit<CR>
nnoremap <leader>gb :Git blame<CR>
nnoremap <leader>gL :Git log --oneline --graph -20<CR>
if g:chopsticks_space_keymaps
nnoremap <leader>gl :Git log --oneline --graph -20<CR>
else
nnoremap <leader>gL :Git log --oneline --graph -20<CR>
endif
endif
" ── Conflict Navigation ────────────────────────────────────────────────────

18
modules/help.vim Normal file
View file

@ -0,0 +1,18 @@
" help.vim — native Vim help entrypoint
function! s:OpenHelp() abort
let l:doc = g:chopsticks_dir . '/doc'
if isdirectory(l:doc)
silent! execute 'helptags ' . fnameescape(l:doc)
endif
try
help chopsticks
catch /^Vim\%((\a\+)\)\=:E149/
echohl WarningMsg
echom 'chopsticks help tags are missing; run :helptags ' . l:doc
echohl None
endtry
endfunction
command! ChopsticksHelp call s:OpenHelp()

View file

@ -14,7 +14,16 @@ let g:vim_markdown_follow_anchor = 1
let g:vim_markdown_new_list_item_indent = 2
let g:vim_markdown_strikethrough = 1
if exists('g:plugs["vim-markdown"]')
function! s:MarkdownKeymaps() abort
if exists('g:plugs["vim-markdown"]')
nnoremap <buffer> <localleader>mt :Toc<CR>
endif
if exists('g:plugs["previm"]')
nnoremap <buffer> <localleader>mp :PrevimOpen<CR>
endif
endfunction
if exists('g:plugs["vim-markdown"]') && !g:chopsticks_space_keymaps
nnoremap <leader>mt :Toc<CR>
endif
@ -24,7 +33,7 @@ elseif executable('xdg-open')
let g:previm_open_cmd = 'xdg-open'
endif
let g:previm_enable_realtime = get(g:, 'previm_enable_realtime', 0)
if exists('g:plugs["previm"]')
if exists('g:plugs["previm"]') && !g:chopsticks_space_keymaps
nnoremap <leader>mp :PrevimOpen<CR>
endif
@ -78,6 +87,7 @@ augroup ChopstickFiletype
autocmd FileType yaml
\ setlocal expandtab shiftwidth=2 tabstop=2
autocmd FileType markdown call s:MarkdownDefaults()
autocmd FileType markdown if g:chopsticks_space_keymaps | call s:MarkdownKeymaps() | endif
autocmd FileType sh
\ setlocal expandtab shiftwidth=2 tabstop=2 textwidth=80
autocmd FileType make

View file

@ -64,7 +64,13 @@ let g:ale_virtualtext_cursor = get(g:, 'ale_virtualtext_cursor', 'disabled')
if exists('g:plugs["ale"]')
nnoremap <silent> [e :ALEPrevious<cr>
nnoremap <silent> ]e :ALENext<cr>
nnoremap <silent> <leader>aD :ALEDetail<cr>
nnoremap <silent> <leader>af :let g:ale_fix_on_save = !g:ale_fix_on_save
\ <bar> echo 'Format on save: ' . (g:ale_fix_on_save ? 'ON' : 'OFF')<cr>
if g:chopsticks_space_keymaps
nnoremap <silent> <leader>xd :ALEDetail<cr>
nnoremap <silent> <leader>uf :let g:ale_fix_on_save = !g:ale_fix_on_save
\ <bar> echo 'Format on save: ' . (g:ale_fix_on_save ? 'ON' : 'OFF')<cr>
else
nnoremap <silent> <leader>aD :ALEDetail<cr>
nnoremap <silent> <leader>af :let g:ale_fix_on_save = !g:ale_fix_on_save
\ <bar> echo 'Format on save: ' . (g:ale_fix_on_save ? 'ON' : 'OFF')<cr>
endif
endif

View file

@ -65,23 +65,42 @@ function! s:on_lsp_buffer_enabled() abort
setlocal signcolumn=yes
endif
nmap <buffer> <leader>dd <plug>(lsp-definition)
nmap <buffer> <leader>dt <plug>(lsp-type-definition)
nmap <buffer> <leader>di <plug>(lsp-implementation)
nmap <buffer> <leader>dr <plug>(lsp-references)
nmap <buffer> <leader>dp <plug>(lsp-previous-diagnostic)
nmap <buffer> <leader>dn <plug>(lsp-next-diagnostic)
if g:chopsticks_space_keymaps
nmap <buffer> gd <plug>(lsp-definition)
nmap <buffer> gr <plug>(lsp-references)
nmap <buffer> gI <plug>(lsp-implementation)
nmap <buffer> gy <plug>(lsp-type-definition)
nmap <buffer> K <plug>(lsp-hover)
nmap <buffer> [d <plug>(lsp-previous-diagnostic)
nmap <buffer> ]d <plug>(lsp-next-diagnostic)
nmap <buffer> <leader>dk <plug>(lsp-hover)
nmap <buffer> <leader>ca <plug>(lsp-code-action)
nmap <buffer> <leader>cr <plug>(lsp-rename)
nmap <buffer> <leader>cf <plug>(lsp-document-format)
xmap <buffer> <leader>cf <plug>(lsp-document-range-format)
nmap <buffer> <leader>rn <plug>(lsp-rename)
nmap <buffer> <leader>ca <plug>(lsp-code-action)
nmap <buffer> <leader>f <plug>(lsp-document-format)
xmap <buffer> <leader>f <plug>(lsp-document-range-format)
nnoremap <buffer> <leader>ci :LspStatus<CR>
nmap <buffer> <leader>co <plug>(lsp-document-symbol-search)
nmap <buffer> <leader>cS <plug>(lsp-workspace-symbol-search)
else
nmap <buffer> <leader>dd <plug>(lsp-definition)
nmap <buffer> <leader>dt <plug>(lsp-type-definition)
nmap <buffer> <leader>di <plug>(lsp-implementation)
nmap <buffer> <leader>dr <plug>(lsp-references)
nmap <buffer> <leader>dp <plug>(lsp-previous-diagnostic)
nmap <buffer> <leader>dn <plug>(lsp-next-diagnostic)
nmap <buffer> <leader>o <plug>(lsp-document-symbol-search)
nmap <buffer> <leader>ws <plug>(lsp-workspace-symbol-search)
nmap <buffer> <leader>cD <plug>(lsp-document-diagnostics)
nmap <buffer> <leader>dk <plug>(lsp-hover)
nmap <buffer> <leader>rn <plug>(lsp-rename)
nmap <buffer> <leader>ca <plug>(lsp-code-action)
nmap <buffer> <leader>f <plug>(lsp-document-format)
xmap <buffer> <leader>f <plug>(lsp-document-range-format)
nmap <buffer> <leader>o <plug>(lsp-document-symbol-search)
nmap <buffer> <leader>ws <plug>(lsp-workspace-symbol-search)
nmap <buffer> <leader>cD <plug>(lsp-document-diagnostics)
endif
endfunction
augroup lsp_install

View file

@ -28,12 +28,29 @@ function! s:ToggleSidebar(...) abort
wincmd p
endfunction
function! s:NavigateWindow(direction) abort
execute 'wincmd ' . a:direction
endfunction
nnoremap <silent> <C-h> :<C-U>call <SID>NavigateWindow('h')<CR>
nnoremap <silent> <C-j> :<C-U>call <SID>NavigateWindow('j')<CR>
nnoremap <silent> <C-k> :<C-U>call <SID>NavigateWindow('k')<CR>
nnoremap <silent> <C-l> :<C-U>call <SID>NavigateWindow('l')<CR>
nnoremap <silent> <leader>e :call <SID>ToggleSidebar()<CR>
nnoremap <silent> <leader>E :call <SID>ToggleSidebar(expand('%:p:h'))<CR>
function! s:NetrwKeymaps() abort
setlocal bufhidden=wipe
nnoremap <buffer> <silent> <C-h> :<C-U>call <SID>NavigateWindow('h')<CR>
nnoremap <buffer> <silent> <C-j> :<C-U>call <SID>NavigateWindow('j')<CR>
nnoremap <buffer> <silent> <C-k> :<C-U>call <SID>NavigateWindow('k')<CR>
nnoremap <buffer> <silent> <C-l> :<C-U>call <SID>NavigateWindow('l')<CR>
endfunction
augroup ChopstickNetrw
autocmd!
autocmd FileType netrw setlocal bufhidden=wipe
autocmd FileType netrw call s:NetrwKeymaps()
augroup END
" ── FZF ─────────────────────────────────────────────────────────────────────
@ -47,21 +64,42 @@ function! s:SmartFiles() abort
endfunction
if exists('g:plugs["fzf.vim"]')
nnoremap <leader>ff :call <SID>SmartFiles()<CR>
nnoremap <leader>b :Buffers<CR>
nnoremap <leader>rg :Rg<CR>
nnoremap <leader>rG :RgWord<CR>
nnoremap <leader>rt :Tags<CR>
nnoremap <leader>gF :GFiles<CR>
nnoremap <leader>fh :History<CR>
nnoremap <leader>fc :Commands<CR>
nnoremap <leader>fm :Marks<CR>
nnoremap <leader>fl :BLines<CR>
nnoremap <leader>fL :Lines<CR>
nnoremap <leader>f/ :History/<CR>
nnoremap <leader>f: :History:<CR>
nnoremap <leader>gC :Commits<CR>
nnoremap <leader>gB :BCommits<CR>
if g:chopsticks_space_keymaps
nnoremap <leader><Space> :call <SID>SmartFiles()<CR>
nnoremap <leader>, :Buffers<CR>
nnoremap <leader>/ :Rg<CR>
nnoremap <leader>ff :call <SID>SmartFiles()<CR>
nnoremap <leader>fb :Buffers<CR>
nnoremap <leader>fg :GFiles<CR>
nnoremap <leader>fr :History<CR>
nnoremap <leader>fl :BLines<CR>
nnoremap <leader>fL :Lines<CR>
nnoremap <leader>s/ :History/<CR>
nnoremap <leader>s: :History:<CR>
nnoremap <leader>sc :Commands<CR>
nnoremap <leader>sm :Marks<CR>
nnoremap <leader>sg :Rg<CR>
nnoremap <leader>sw :RgWord<CR>
nnoremap <leader>st :Tags<CR>
nnoremap <leader>gC :Commits<CR>
nnoremap <leader>gB :BCommits<CR>
else
nnoremap <leader>ff :call <SID>SmartFiles()<CR>
nnoremap <leader>b :Buffers<CR>
nnoremap <leader>rg :Rg<CR>
nnoremap <leader>rG :RgWord<CR>
nnoremap <leader>rt :Tags<CR>
nnoremap <leader>gF :GFiles<CR>
nnoremap <leader>fh :History<CR>
nnoremap <leader>fc :Commands<CR>
nnoremap <leader>fm :Marks<CR>
nnoremap <leader>fl :BLines<CR>
nnoremap <leader>fL :Lines<CR>
nnoremap <leader>f/ :History/<CR>
nnoremap <leader>f: :History:<CR>
nnoremap <leader>gC :Commits<CR>
nnoremap <leader>gB :BCommits<CR>
endif
endif
let g:fzf_layout = { 'down': '40%' }
@ -99,18 +137,27 @@ function! s:ToggleMaximize() abort
echo 'Window: MAXIMIZED'
endif
endfunction
nnoremap <silent> <leader>z :call <SID>ToggleMaximize()<CR>
if g:chopsticks_space_keymaps
nnoremap <silent> <leader>z :call <SID>ToggleMaximize()<CR>
else
nnoremap <silent> <leader>z :call <SID>ToggleMaximize()<CR>
endif
" ── Terminal ────────────────────────────────────────────────────────────────
if has('terminal')
nnoremap <leader>tv :terminal<CR>
nnoremap <leader>th :terminal ++rows=10<CR>
if g:chopsticks_space_keymaps
nnoremap <leader>tt :terminal<CR>
nnoremap <leader>th :terminal ++rows=10<CR>
else
nnoremap <leader>tv :terminal<CR>
nnoremap <leader>th :terminal ++rows=10<CR>
endif
if g:chopsticks_enable_terminal_keymaps
tnoremap <Esc><Esc> <C-\><C-n>
tnoremap <C-h> <C-\><C-n><C-w>h
tnoremap <C-j> <C-\><C-n><C-w>j
tnoremap <C-k> <C-\><C-n><C-w>k
tnoremap <C-l> <C-\><C-n><C-w>l
tnoremap <C-h> <C-\><C-n>:<C-U>call <SID>NavigateWindow('h')<CR>
tnoremap <C-j> <C-\><C-n>:<C-U>call <SID>NavigateWindow('j')<CR>
tnoremap <C-k> <C-\><C-n>:<C-U>call <SID>NavigateWindow('k')<CR>
tnoremap <C-l> <C-\><C-n>:<C-U>call <SID>NavigateWindow('l')<CR>
endif
endif

View file

@ -62,7 +62,7 @@ if g:chopsticks_enable_ui_extras
Plug 'mhinz/vim-startify'
endif
Plug 'lifepillar/vim-solarized8'
if !empty($TMUX)
if g:chopsticks_enable_tmux_navigator && !empty($TMUX)
Plug 'christoomey/vim-tmux-navigator'
endif

10
modules/quickfix.vim Normal file
View file

@ -0,0 +1,10 @@
" quickfix.vim — quickfix and location-list helpers
augroup ChopstickQF
autocmd!
autocmd QuickFixCmdPost [^l]* cwindow
autocmd QuickFixCmdPost l* lwindow
augroup END
nnoremap <silent> ]q :cnext<CR>
nnoremap <silent> [q :cprev<CR>

28
modules/runner.vim Normal file
View file

@ -0,0 +1,28 @@
" runner.vim — run the current file by filetype
function! s:RunFile() abort
write
let l:ft = &filetype
let l:file = shellescape(expand('%:p'))
if l:ft ==# 'python' | execute '!python3 ' . l:file
elseif l:ft ==# 'javascript' | execute '!node ' . l:file
elseif l:ft ==# 'typescript' | execute '!npx ts-node ' . l:file
elseif l:ft ==# 'go' | execute '!go run ' . l:file
elseif l:ft ==# 'rust' | execute '!cargo run'
elseif l:ft ==# 'sh' | execute '!bash ' . l:file
elseif l:ft ==# 'c'
let l:out_path = tempname()
let l:out = shellescape(l:out_path)
execute '!gcc -o ' . l:out . ' ' . l:file . ' && ' . l:out
call delete(l:out_path)
elseif l:ft ==# 'lua' | execute '!lua ' . l:file
elseif l:ft ==# 'ruby' | execute '!ruby ' . l:file
elseif l:ft ==# 'perl' | execute '!perl ' . l:file
else | echo 'No runner for filetype: ' . l:ft
endif
endfunction
if g:chopsticks_space_keymaps
nnoremap <leader>rr :call <SID>RunFile()<CR>
else
nnoremap <leader>cr :call <SID>RunFile()<CR>
endif

199
modules/status.vim Normal file
View file

@ -0,0 +1,199 @@
" status.vim — health diagnostics
function! s:Check(name, cmd) abort
return executable(a:cmd) ? ' OK ' . a:name : ' -- ' . a:name . ' (missing: ' . a:cmd . ')'
endfunction
function! s:Off(name, reason) abort
return ' off ' . a:name . ' (' . a:reason . ')'
endfunction
function! s:PlugDir(name) abort
if !exists('g:plugs') || !has_key(g:plugs, a:name)
return ''
endif
return fnamemodify(get(g:plugs[a:name], 'dir', ''), ':p')
endfunction
function! s:PlugInstalled(name) abort
let l:dir = s:PlugDir(a:name)
return !empty(l:dir) && isdirectory(l:dir)
endfunction
function! s:LspStackIssue() abort
if !get(g:, 'chopsticks_enable_lsp', 1)
return 'LSP disabled by profile'
endif
if empty(s:PlugDir('vim-lsp'))
return 'vim-lsp not declared by this profile'
endif
if !s:PlugInstalled('vim-lsp')
return 'vim-lsp not installed; run :PlugInstall'
endif
if empty(s:PlugDir('vim-lsp-settings'))
return 'vim-lsp-settings not declared by this profile'
endif
if !s:PlugInstalled('vim-lsp-settings')
return 'vim-lsp-settings not installed; run :PlugInstall'
endif
return ''
endfunction
function! s:LspStackCheck() abort
let l:issue = s:LspStackIssue()
if l:issue ==# 'LSP disabled by profile'
return s:Off('vim-lsp stack', l:issue)
endif
if !empty(l:issue)
return ' -- vim-lsp stack (' . l:issue . ')'
endif
if exists(':LspStatus') == 2 || exists(':LspInstallServer') == 2
return ' OK vim-lsp stack (installed)'
endif
return ' OK vim-lsp stack (installed; not loaded yet)'
endfunction
function! s:LspCheck(ft, server) abort
let l:issue = s:LspStackIssue()
if l:issue ==# 'LSP disabled by profile'
return s:Off(a:ft, l:issue)
endif
if !empty(l:issue)
return ' -- ' . a:ft . ' (' . l:issue . ')'
endif
let l:dir = expand('~/.local/share/vim-lsp-settings/servers/' . a:server)
if isdirectory(l:dir)
return ' OK ' . a:ft . ' (' . a:server . ')'
endif
return ' -- ' . a:ft . ' (:LspInstallServer in a ' . a:ft . ' file)'
endfunction
function! s:BetaLogPath() abort
let l:configured = get(g:, 'chopsticks_beta_log', '')
if !empty(l:configured)
return expand(l:configured)
endif
let l:xdg = !empty($XDG_CONFIG_HOME) && $XDG_CONFIG_HOME =~# '^/'
\ ? $XDG_CONFIG_HOME
\ : '~/.config'
return expand(l:xdg . '/chopsticks-2.3.0.md')
endfunction
function! s:LocalConfigPath() abort
let l:xdg = !empty($XDG_CONFIG_HOME) && $XDG_CONFIG_HOME =~# '^/'
\ ? $XDG_CONFIG_HOME
\ : '~/.config'
return expand(get(g:, 'chopsticks_resolved_local_config',
\ get(g:, 'chopsticks_local_config', l:xdg . '/chopsticks.vim')))
endfunction
function! s:ChopsticksStatus() abort
let l:lines = []
call add(l:lines, 'chopsticks status')
call add(l:lines, repeat('─', 50))
call add(l:lines, '')
call add(l:lines, ' help :ChopsticksHelp :ChopsticksTutor SPC ?')
call add(l:lines, ' config ' . s:LocalConfigPath())
call add(l:lines, ' commands :ChopsticksConfig :ChopsticksReload')
call add(l:lines, '')
if !empty(get(g:, 'chopsticks_beta_label', ''))
call add(l:lines, '── release candidate ──')
call add(l:lines, ' candidate ' . g:chopsticks_beta_label)
call add(l:lines, ' keymap ' . (get(g:, 'chopsticks_space_keymaps', 0) ? 'space' : 'classic'))
call add(l:lines, ' log ' . s:BetaLogPath())
call add(l:lines, ' commands :ChopsticksBeta :ChopsticksBetaLog')
call add(l:lines, ' :ChopsticksBetaSession')
call add(l:lines, '')
endif
call add(l:lines, '── system tools ──')
call add(l:lines, s:Check('fzf', 'fzf'))
call add(l:lines, s:Check('ripgrep', 'rg'))
call add(l:lines, s:Check('git', 'git'))
call add(l:lines, s:Check('curl', 'curl'))
call add(l:lines, s:Check('node', 'node'))
call add(l:lines, s:Check('python3', 'python3'))
call add(l:lines, s:Check('go', 'go'))
call add(l:lines, '')
call add(l:lines, '── lsp servers ── (:LspInstallServer to install)')
call add(l:lines, s:LspStackCheck())
if get(g:, 'chopsticks_enable_lsp', 1)
call add(l:lines, ' LSP actions are buffer-local and start after a server attaches.')
call add(l:lines, ' Missing one? Open that filetype and run :LspInstallServer once.')
endif
call add(l:lines, s:LspCheck('python', 'pylsp'))
call add(l:lines, s:LspCheck('go', 'gopls'))
call add(l:lines, s:LspCheck('rust', 'rust-analyzer'))
call add(l:lines, s:LspCheck('typescript', 'typescript-language-server'))
call add(l:lines, s:LspCheck('c/c++', 'clangd'))
call add(l:lines, s:LspCheck('bash', 'bash-language-server'))
call add(l:lines, s:LspCheck('html', 'vscode-html-language-server'))
call add(l:lines, s:LspCheck('json', 'vscode-json-language-server'))
call add(l:lines, s:LspCheck('yaml', 'yaml-language-server'))
call add(l:lines, s:LspCheck('markdown', 'marksman'))
call add(l:lines, s:LspCheck('sql', 'sqls'))
call add(l:lines, '')
call add(l:lines, '── linters ──')
if get(g:, 'chopsticks_enable_lint', 1)
call add(l:lines, s:Check('flake8 (python)', 'flake8'))
call add(l:lines, s:Check('pylint (python)', 'pylint'))
call add(l:lines, s:Check('eslint (js/ts)', 'eslint'))
call add(l:lines, s:Check('staticcheck (go)', 'staticcheck'))
call add(l:lines, s:Check('shellcheck (sh)', 'shellcheck'))
call add(l:lines, s:Check('yamllint (yaml)', 'yamllint'))
call add(l:lines, s:Check('hadolint (docker)', 'hadolint'))
if get(g:, 'chopsticks_markdown_lint', 0)
call add(l:lines, s:Check('markdownlint (md)', 'markdownlint'))
else
call add(l:lines, s:Off('markdownlint (md)', 'disabled by default'))
endif
else
call add(l:lines, s:Off('ALE linters', 'lint disabled by profile'))
endif
call add(l:lines, '')
call add(l:lines, '── formatters ── (format-on-save is ' . (get(g:, 'ale_fix_on_save', 0) ? 'ON' : 'OFF') . ')')
if get(g:, 'chopsticks_enable_lint', 1)
call add(l:lines, s:Check('black (python)', 'black'))
call add(l:lines, s:Check('isort (python)', 'isort'))
call add(l:lines, s:Check('prettier (js/ts/json)', 'prettier'))
if get(g:, 'chopsticks_markdown_format_on_save', 0)
call add(l:lines, s:Check('prettier (md)', 'prettier'))
else
call add(l:lines, s:Off('prettier (md)', 'disabled by default'))
endif
call add(l:lines, s:Check('goimports (go)', 'goimports'))
call add(l:lines, s:Check('rustfmt (rust)', 'rustfmt'))
call add(l:lines, s:Check('clang-format (c)', 'clang-format'))
else
call add(l:lines, s:Off('ALE formatters', 'lint disabled by profile'))
endif
call add(l:lines, '')
let l:ok = len(filter(copy(l:lines), 'v:val =~# " OK "'))
let l:miss = len(filter(copy(l:lines), 'v:val =~# " -- "'))
call add(l:lines, repeat('─', 50))
call add(l:lines, ' ' . l:ok . ' ready, ' . l:miss . ' missing')
call add(l:lines, '')
call add(l:lines, ' Install missing tools with ./install.sh')
if get(g:, 'chopsticks_enable_lsp', 1)
call add(l:lines, ' Install LSP servers with :LspInstallServer')
endif
let l:name = '__ChopsticksStatus__'
if bufwinnr(l:name) > 0
execute bufwinnr(l:name) . 'wincmd w | bd'
endif
execute 'botright new ' . l:name
resize 45
setlocal buftype=nofile bufhidden=wipe nobuflisted noswapfile
setlocal nowrap nonumber norelativenumber signcolumn=no
call setline(1, l:lines)
setlocal nomodifiable readonly
nnoremap <buffer> <silent> q :bd<CR>
endfunction
command! ChopsticksStatus call s:ChopsticksStatus()

View file

@ -1,445 +1,4 @@
" tools.vim — run file, sudo save, quickfix, helpers
" ── Buffer Close ───────────────────────────────────────────────────────────
command! Bclose call <SID>BufcloseCloseIt()
function! <SID>BufcloseCloseIt()
let l:currentBufNum = bufnr("%")
let l:alternateBufNum = bufnr("#")
if buflisted(l:alternateBufNum)
buffer #
else
bnext
endif
if bufnr("%") == l:currentBufNum
new
endif
if buflisted(l:currentBufNum)
execute("bdelete! " . l:currentBufNum)
endif
endfunction
" ── Utilities ──────────────────────────────────────────────────────────────
if get(g:, 'chopsticks_enable_reindent_file', 0)
nnoremap <leader>F gg=G``
endif
vnoremap <leader>F =
nnoremap <leader>wa :wa<CR>
nnoremap <silent> <Leader>= :exe "resize " . (winheight(0) * 3/2)<CR>
nnoremap <silent> <Leader>- :exe "resize " . (winheight(0) * 2/3)<CR>
nnoremap <leader><leader> <c-^>
nnoremap <leader>W :%s/\s\+$//<CR>:let @/=''<CR>
vnoremap <leader>W :s/\s\+$//<CR>:let @/=''<CR>gv
nnoremap <leader>ev :edit $MYVIMRC<CR>
nnoremap <leader>sv :unlet! g:chopsticks_loaded<CR>:execute 'source ' . fnameescape($MYVIMRC)<CR>:echo "vimrc reloaded"<CR>
nnoremap <leader>* :%s/\<<C-r><C-w>\>//g<Left><Left>
vnoremap <leader>* :s///g<Left><Left><Left>
if has('clipboard')
nnoremap <leader>cp :let @+ = expand("%:p")<CR>:echo "Copied: " . expand("%:p")<CR>
nnoremap <leader>cf :let @+ = expand("%:t")<CR>:echo "Copied: " . expand("%:t")<CR>
endif
" ── Auto-Create Directories ─────────────────────────────────────────────────
function! s:MkNonExDir(file, buf)
if empty(getbufvar(a:buf, '&buftype')) && a:file !~# '\v^\w+\:\/'
let dir = fnamemodify(a:file, ':h')
if !isdirectory(dir)
call mkdir(dir, 'p')
endif
endif
endfunction
augroup BWCCreateDir
autocmd!
autocmd BufWritePre *
\ if !empty(expand('<afile>')) |
\ call s:MkNonExDir(expand('<afile>'), +expand('<abuf>')) |
\ endif
augroup END
" ── Large File Handling ──────────────────────────────────────────────────────
let g:LargeFile = get(g:, 'LargeFile', 1024 * 1024 * 10)
let s:tty_large = g:is_tty ? 512000 : g:LargeFile
function! s:ApplyLargeFileSettings() abort
if get(b:, 'chopsticks_large_file', 0)
setlocal bufhidden=unload undolevels=-1 noswapfile
let b:ale_enabled = 0
if &l:syntax !=# ''
setlocal syntax=
endif
elseif get(b:, 'chopsticks_tty_large_file', 0)
if &l:syntax !=# ''
setlocal syntax=
endif
endif
endfunction
function! s:MarkLargeFile(file) abort
if empty(a:file)
return
endif
let l:fsize = getfsize(a:file)
if l:fsize > g:LargeFile || l:fsize == -2
let b:chopsticks_large_file = 1
elseif g:is_tty && l:fsize > s:tty_large
let b:chopsticks_tty_large_file = 1
endif
call s:ApplyLargeFileSettings()
endfunction
augroup ChopstickLargeFile
autocmd!
autocmd BufReadPre * call s:MarkLargeFile(expand('<afile>'))
autocmd BufReadPost,FileType,Syntax * call s:ApplyLargeFileSettings()
augroup END
" ── Run Current File (,cr) ──────────────────────────────────────────────────
function! s:RunFile() abort
write
let l:ft = &filetype
let l:file = shellescape(expand('%:p'))
if l:ft ==# 'python' | execute '!python3 ' . l:file
elseif l:ft ==# 'javascript' | execute '!node ' . l:file
elseif l:ft ==# 'typescript' | execute '!npx ts-node ' . l:file
elseif l:ft ==# 'go' | execute '!go run ' . l:file
elseif l:ft ==# 'rust' | execute '!cargo run'
elseif l:ft ==# 'sh' | execute '!bash ' . l:file
elseif l:ft ==# 'c'
let l:out_path = tempname()
let l:out = shellescape(l:out_path)
execute '!gcc -o ' . l:out . ' ' . l:file . ' && ' . l:out
call delete(l:out_path)
elseif l:ft ==# 'lua' | execute '!lua ' . l:file
elseif l:ft ==# 'ruby' | execute '!ruby ' . l:file
elseif l:ft ==# 'perl' | execute '!perl ' . l:file
else | echo 'No runner for filetype: ' . l:ft
endif
endfunction
nnoremap <leader>cr :call <SID>RunFile()<CR>
" ── Sudo Save ───────────────────────────────────────────────────────────────
if get(g:, 'chopsticks_enable_sudo_save_bang', 0)
cnoremap w!! w !sudo tee > /dev/null %
endif
" ── QuickFix ────────────────────────────────────────────────────────────────
augroup ChopstickQF
autocmd!
autocmd QuickFixCmdPost [^l]* cwindow
autocmd QuickFixCmdPost l* lwindow
augroup END
nnoremap <silent> ]q :cnext<CR>
nnoremap <silent> [q :cprev<CR>
" ── Status Diagnostic (:ChopsticksStatus) ───────────────────────────────────
function! s:Check(name, cmd) abort
return executable(a:cmd) ? ' OK ' . a:name : ' -- ' . a:name . ' (missing: ' . a:cmd . ')'
endfunction
function! s:Off(name, reason) abort
return ' off ' . a:name . ' (' . a:reason . ')'
endfunction
function! s:PlugDir(name) abort
if !exists('g:plugs') || !has_key(g:plugs, a:name)
return ''
endif
return fnamemodify(get(g:plugs[a:name], 'dir', ''), ':p')
endfunction
function! s:PlugInstalled(name) abort
let l:dir = s:PlugDir(a:name)
return !empty(l:dir) && isdirectory(l:dir)
endfunction
function! s:LspStackIssue() abort
if !get(g:, 'chopsticks_enable_lsp', 1)
return 'LSP disabled by profile'
endif
if empty(s:PlugDir('vim-lsp'))
return 'vim-lsp not declared by this profile'
endif
if !s:PlugInstalled('vim-lsp')
return 'vim-lsp not installed; run :PlugInstall'
endif
if empty(s:PlugDir('vim-lsp-settings'))
return 'vim-lsp-settings not declared by this profile'
endif
if !s:PlugInstalled('vim-lsp-settings')
return 'vim-lsp-settings not installed; run :PlugInstall'
endif
return ''
endfunction
function! s:LspStackCheck() abort
let l:issue = s:LspStackIssue()
if l:issue ==# 'LSP disabled by profile'
return s:Off('vim-lsp stack', l:issue)
endif
if !empty(l:issue)
return ' -- vim-lsp stack (' . l:issue . ')'
endif
if exists(':LspStatus') == 2 || exists(':LspInstallServer') == 2
return ' OK vim-lsp stack (installed)'
endif
return ' OK vim-lsp stack (installed; not loaded yet)'
endfunction
function! s:LspCheck(ft, server) abort
let l:issue = s:LspStackIssue()
if l:issue ==# 'LSP disabled by profile'
return s:Off(a:ft, l:issue)
endif
if !empty(l:issue)
return ' -- ' . a:ft . ' (' . l:issue . ')'
endif
let l:dir = expand('~/.local/share/vim-lsp-settings/servers/' . a:server)
if isdirectory(l:dir)
return ' OK ' . a:ft . ' (' . a:server . ')'
endif
return ' -- ' . a:ft . ' (:LspInstallServer in a ' . a:ft . ' file)'
endfunction
function! s:ChopsticksStatus() abort
let l:lines = []
call add(l:lines, 'chopsticks status')
call add(l:lines, repeat('─', 50))
call add(l:lines, '')
call add(l:lines, '── system tools ──')
call add(l:lines, s:Check('fzf', 'fzf'))
call add(l:lines, s:Check('ripgrep', 'rg'))
call add(l:lines, s:Check('git', 'git'))
call add(l:lines, s:Check('curl', 'curl'))
call add(l:lines, s:Check('node', 'node'))
call add(l:lines, s:Check('python3', 'python3'))
call add(l:lines, s:Check('go', 'go'))
call add(l:lines, '')
call add(l:lines, '── lsp servers ── (:LspInstallServer to install)')
call add(l:lines, s:LspStackCheck())
if get(g:, 'chopsticks_enable_lsp', 1)
call add(l:lines, ' LSP actions are buffer-local and start after a server attaches.')
call add(l:lines, ' Missing one? Open that filetype and run :LspInstallServer once.')
endif
call add(l:lines, s:LspCheck('python', 'pylsp'))
call add(l:lines, s:LspCheck('go', 'gopls'))
call add(l:lines, s:LspCheck('rust', 'rust-analyzer'))
call add(l:lines, s:LspCheck('typescript', 'typescript-language-server'))
call add(l:lines, s:LspCheck('c/c++', 'clangd'))
call add(l:lines, s:LspCheck('bash', 'bash-language-server'))
call add(l:lines, s:LspCheck('html', 'vscode-html-language-server'))
call add(l:lines, s:LspCheck('json', 'vscode-json-language-server'))
call add(l:lines, s:LspCheck('yaml', 'yaml-language-server'))
call add(l:lines, s:LspCheck('markdown', 'marksman'))
call add(l:lines, s:LspCheck('sql', 'sqls'))
call add(l:lines, '')
call add(l:lines, '── linters ──')
if get(g:, 'chopsticks_enable_lint', 1)
call add(l:lines, s:Check('flake8 (python)', 'flake8'))
call add(l:lines, s:Check('pylint (python)', 'pylint'))
call add(l:lines, s:Check('eslint (js/ts)', 'eslint'))
call add(l:lines, s:Check('staticcheck (go)', 'staticcheck'))
call add(l:lines, s:Check('shellcheck (sh)', 'shellcheck'))
call add(l:lines, s:Check('yamllint (yaml)', 'yamllint'))
call add(l:lines, s:Check('hadolint (docker)', 'hadolint'))
if get(g:, 'chopsticks_markdown_lint', 0)
call add(l:lines, s:Check('markdownlint (md)', 'markdownlint'))
else
call add(l:lines, s:Off('markdownlint (md)', 'disabled by default'))
endif
else
call add(l:lines, s:Off('ALE linters', 'lint disabled by profile'))
endif
call add(l:lines, '')
call add(l:lines, '── formatters ── (format-on-save is ' . (get(g:, 'ale_fix_on_save', 0) ? 'ON' : 'OFF') . ')')
if get(g:, 'chopsticks_enable_lint', 1)
call add(l:lines, s:Check('black (python)', 'black'))
call add(l:lines, s:Check('isort (python)', 'isort'))
call add(l:lines, s:Check('prettier (js/ts/json)', 'prettier'))
if get(g:, 'chopsticks_markdown_format_on_save', 0)
call add(l:lines, s:Check('prettier (md)', 'prettier'))
else
call add(l:lines, s:Off('prettier (md)', 'disabled by default'))
endif
call add(l:lines, s:Check('goimports (go)', 'goimports'))
call add(l:lines, s:Check('rustfmt (rust)', 'rustfmt'))
call add(l:lines, s:Check('clang-format (c)', 'clang-format'))
else
call add(l:lines, s:Off('ALE formatters', 'lint disabled by profile'))
endif
call add(l:lines, '')
let l:ok = len(filter(copy(l:lines), 'v:val =~# " OK "'))
let l:miss = len(filter(copy(l:lines), 'v:val =~# " -- "'))
call add(l:lines, repeat('─', 50))
call add(l:lines, ' ' . l:ok . ' ready, ' . l:miss . ' missing')
call add(l:lines, '')
call add(l:lines, ' Install missing tools with ./install.sh')
if get(g:, 'chopsticks_enable_lsp', 1)
call add(l:lines, ' Install LSP servers with :LspInstallServer')
endif
let l:name = '__ChopsticksStatus__'
if bufwinnr(l:name) > 0
execute bufwinnr(l:name) . 'wincmd w | bd'
endif
execute 'botright new ' . l:name
resize 45
setlocal buftype=nofile bufhidden=wipe nobuflisted noswapfile
setlocal nowrap nonumber norelativenumber signcolumn=no
call setline(1, l:lines)
setlocal nomodifiable readonly
nnoremap <buffer> <silent> q :bd<CR>
endfunction
command! ChopsticksStatus call s:ChopsticksStatus()
" ── Cheat Sheet (,?) ────────────────────────────────────────────────────────
function! s:CheatSheet() abort
let l:name = '__ChopsticksCheatSheet__'
if bufwinnr(l:name) > 0
execute bufwinnr(l:name) . 'wincmd w | bd'
return
endif
let l:has_lsp = get(g:, 'chopsticks_enable_lsp', 1)
let l:has_lint = get(g:, 'chopsticks_enable_lint', 1)
let l:has_undotree = exists('g:plugs["undotree"]')
let l:has_previm = exists('g:plugs["previm"]')
let l:lines = [
\ ' chopsticks ,? close',
\ ' ─────────────────────────────',
\ '',
\ ' ── files ──────────────────',
\ ' ,ff files',
\ ' ,b buffers',
\ ' ,rg grep project',
\ ' ,rG grep word',
\ ' ,e sidebar (cwd)',
\ ' ,E sidebar (file dir)',
\ ' ,, last file',
\ ' ,fh recent files',
\ ' ,fl lines in buffer',
\ ' ,fc commands',
\ ' ,fm marks',
\ '',
\ ' ── code ──────────────────',
\ ]
if l:has_lsp
call extend(l:lines, [
\ ' ,dd definition',
\ ' ,dt type definition',
\ ' ,di implementation',
\ ' ,dr references',
\ ' ,dk hover docs',
\ ' ,rn rename',
\ ' ,ca code action',
\ ' ,f format',
\ ' ,o outline',
\ ' ,dp ,dn LSP diagnostics',
\ ' :LspInstallServer setup LSP',
\ ' :ChopsticksStatus check LSP setup',
\ ])
endif
call add(l:lines, ' ,cr run file')
if l:has_previm
call add(l:lines, ' ,mp markdown preview')
endif
call add(l:lines, ' ,mt table of contents')
if l:has_lint
call extend(l:lines, [
\ ' [e ]e ALE errors',
\ ' ,af format on save',
\ ])
endif
call extend(l:lines, [
\ '',
\ ' ── edit ──────────────────',
\ ' gc comment',
\ ' ,S+2ch easymotion jump',
\ ' cs"'' surround',
\ ])
if l:has_undotree
call add(l:lines, ' ,u undo tree')
endif
call extend(l:lines, [
\ ' ,y ,p clipboard y/p (v)',
\ ' Alt+j/k move line (v)',
\ ' ,* replace word (v)',
\ ' ,F re-indent (v)',
\ ' ,W strip trailing (v)',
\ '',
\ ' ── git ───────────────────',
\ ' ,gs status',
\ ' ,gd diff',
\ ' ,gb blame',
\ ' ,gc commit',
\ ' ,gp push',
\ ' ,gl pull',
\ ' ,gL log graph',
\ ' ,gC FZF commits',
\ ' [x ]x conflict markers',
\ '',
\ ' ── windows ───────────────',
\ ' <C-w>hjkl navigate splits',
\ ' ,h ,l prev / next buf',
\ ' ,bd close buffer',
\ ' ,z maximize toggle',
\ ' ,= ,- resize height',
\ ' ,tv ,th terminal v / h',
\ ' ]q [q next / prev qf',
\ ' ,qo ,qc open / close qf',
\ '',
\ ' ── toggle ────────────────',
\ ' F2 paste mode',
\ ' F3 line numbers',
\ ' F4 relative numbers',
\ ' F6 invisible chars',
\ ' ,ss spell check',
\ '',
\ ' ── survival ──────────────',
\ ' ,w save',
\ ' ,q quit',
\ ' ,x save + quit',
\ ' Esc exit insert',
\ ' ,ev edit vimrc',
\ ' ,sv reload vimrc',
\ ' :ChopsticksStatus health',
\ ])
execute 'vertical botright new ' . l:name
vertical resize 42
setlocal buftype=nofile bufhidden=wipe nobuflisted noswapfile
setlocal nowrap nonumber norelativenumber signcolumn=no
setlocal winfixwidth
call setline(1, l:lines)
setlocal nomodifiable readonly
nnoremap <buffer> <silent> q :bd<CR>
nnoremap <buffer> <silent> <leader>? :bd<CR>
endfunction
nnoremap <silent> <leader>? :call <SID>CheatSheet()<CR>
" tools.vim — compatibility placeholder
"
" Tooling was split into granular modules:
" buffers, utilities, files, runner, quickfix, status, cheatsheet, and tutor.

134
modules/tutor.vim Normal file
View file

@ -0,0 +1,134 @@
" tutor.vim — guided practice for chopsticks keymaps
function! s:OpenTutor(lines) abort
let l:name = '__ChopsticksTutor__'
if bufwinnr(l:name) > 0
execute bufwinnr(l:name) . 'wincmd w | bd'
return 0
endif
execute 'botright new ' . l:name
resize 38
setlocal buftype=nofile bufhidden=wipe nobuflisted noswapfile
setlocal nowrap nonumber norelativenumber signcolumn=no
call setline(1, a:lines)
setlocal nomodifiable readonly
nnoremap <buffer> <silent> q :bd<CR>
return 1
endfunction
function! s:ChopsticksTutor() abort
if g:chopsticks_space_keymaps
let l:lines = [
\ ' chopsticks tutor q close',
\ ' ───────────────────────────────',
\ '',
\ ' Goal: train one long-term project loop around Vim.',
\ ' Keep Vim editing habits; standardize the surrounding work.',
\ '',
\ ' 1. trained loop',
\ ' SPC SPC open a project file',
\ ' s + 2 chars jump to visible text',
\ ' gd / gr / K inspect definition / refs / docs',
\ ' SPC rr run current file',
\ ' SPC / grep project',
\ ' SPC gs check git status',
\ ' SPC ? active cheat sheet',
\ '',
\ ' 2. survival',
\ ' Esc Normal mode',
\ ' SPC w save',
\ ' SPC q quit',
\ ' :x / ZZ save and quit',
\ ' SPC fc edit local config',
\ ' SPC fV reload config',
\ ' :ChopsticksHelp full help',
\ ' :ChopsticksConfig local config',
\ ' :ChopsticksReload reload config',
\ ' :ChopsticksStatus health check',
\ ' :ChopsticksBeta release checklist',
\ ' :ChopsticksBetaLog release notes',
\ ' :ChopsticksBetaSession new note',
\ '',
\ ' 3. find and switch',
\ ' SPC SPC find files',
\ ' SPC / grep project',
\ ' SPC , buffers',
\ ' SPC Tab alternate buffer',
\ ' SPC e/E sidebar cwd / file dir',
\ '',
\ ' 4. jump and edit',
\ ' s + 2 chars visible jump',
\ ' SPC S same jump fallback',
\ ' cl / cc native s / S substitute',
\ ' gc comment',
\ ' SPC U undo tree',
\ '',
\ ' 5. code loop',
\ ' gd / gr / K definition / refs / docs',
\ ' gI / gy implementation / type',
\ ' [d ]d LSP diagnostics',
\ ' SPC ca/cr/cf action / rename / format',
\ ' SPC rr run current file',
\ '',
\ ' 6. git and windows',
\ ' SPC gs/gd/gb status / diff / blame',
\ ' SPC gl log graph',
\ ' Ctrl-h/j/k/l split navigation',
\ ' <C-w>hjkl native fallback',
\ ' SPC e, Ctrl-h/l enter/leave sidebar',
\ ' SPC z maximize split',
\ '',
\ ' daily drill',
\ ' Repeat: SPC SPC, s, gd/K, edit, SPC rr, SPC /, SPC gs.',
\ ]
else
let l:lines = [
\ ' chopsticks tutor q close',
\ ' ───────────────────────────────',
\ '',
\ ' Goal: train one long-term project loop around Vim.',
\ '',
\ ' classic layout',
\ ' ,? active cheat sheet',
\ ' ,w / ,x save / save and quit',
\ ' ,ff find files',
\ ' ,rg grep project',
\ ' ,b buffers',
\ ' ,, alternate buffer',
\ '',
\ ' code loop',
\ ' ,dd / ,dr definition / refs',
\ ' ,dk hover docs',
\ ' ,ca / ,rn action / rename',
\ ' ,f format',
\ ' ,cr run current file',
\ '',
\ ' edit and git',
\ ' ,S + 2 chars EasyMotion jump',
\ ' gc comment',
\ ' ,u undo tree',
\ ' ,gs/,gd/,gb status / diff / blame',
\ ' Ctrl-h/j/k/l split navigation',
\ ' <C-w>hjkl native fallback',
\ '',
\ ' support',
\ ' ,ec edit local config',
\ ' ,sv reload config',
\ ' :ChopsticksHelp full help',
\ ' :ChopsticksConfig local config',
\ ' :ChopsticksReload reload config',
\ ' :ChopsticksStatus health check',
\ ' :ChopsticksBeta release checklist',
\ ' :ChopsticksBetaLog release notes',
\ ' :ChopsticksBetaSession new note',
\ ' README.md full reference',
\ ' QUICKSTART.md 5-minute path',
\ ]
endif
if s:OpenTutor(l:lines)
nnoremap <buffer> <silent> ? :ChopsticksCheatSheet<CR>
endif
endfunction
command! ChopsticksTutor call s:ChopsticksTutor()

108
modules/utilities.vim Normal file
View file

@ -0,0 +1,108 @@
" utilities.vim — small editing and config helpers
function! s:LocalConfigPath() abort
let l:xdg = !empty($XDG_CONFIG_HOME) && $XDG_CONFIG_HOME =~# '^/'
\ ? $XDG_CONFIG_HOME
\ : '~/.config'
return expand(get(g:, 'chopsticks_resolved_local_config',
\ get(g:, 'chopsticks_local_config', l:xdg . '/chopsticks.vim')))
endfunction
function! s:EditLocalConfig() abort
let l:path = s:LocalConfigPath()
let l:new_file = !filereadable(l:path)
let l:dir = fnamemodify(l:path, ':h')
if !isdirectory(l:dir)
call mkdir(l:dir, 'p')
endif
execute 'edit ' . fnameescape(l:path)
setlocal filetype=vim
if l:new_file && line('$') == 1 && getline(1) ==# ''
call setline(1, [
\ '" chopsticks local preferences',
\ "let g:chopsticks_profile = 'engineer'",
\ "let g:chopsticks_keymap_style = 'space'",
\ '',
\ '" Optional habits:',
\ '" let g:chopsticks_enable_jk_escape = 1',
\ '" let g:chopsticks_enable_ctrl_s_save = 1',
\ '" let g:chopsticks_enable_auto_pairs = 1',
\ ])
setlocal nomodified
endif
endfunction
function! s:ReloadChopsticks() abort
unlet! g:chopsticks_loaded
execute 'source ' . fnameescape($MYVIMRC)
echo 'chopsticks reloaded'
endfunction
command! ChopsticksConfig call s:EditLocalConfig()
command! ChopsticksReload call s:ReloadChopsticks()
if get(g:, 'chopsticks_enable_reindent_file', 0)
if g:chopsticks_space_keymaps
nnoremap <leader>c= gg=G``
else
nnoremap <leader>F gg=G``
endif
endif
if g:chopsticks_space_keymaps
vnoremap <leader>= =
else
vnoremap <leader>F =
nnoremap <leader>wa :wa<CR>
endif
if !g:chopsticks_space_keymaps
nnoremap <silent> <Leader>= :exe "resize " . (winheight(0) * 3/2)<CR>
nnoremap <silent> <Leader>- :exe "resize " . (winheight(0) * 2/3)<CR>
endif
if g:chopsticks_space_keymaps
nnoremap <leader><Tab> <c-^>
else
nnoremap <leader><leader> <c-^>
endif
if g:chopsticks_space_keymaps
nnoremap <leader>cW :%s/\s\+$//<CR>:let @/=''<CR>
vnoremap <leader>cW :s/\s\+$//<CR>:let @/=''<CR>gv
else
nnoremap <leader>W :%s/\s\+$//<CR>:let @/=''<CR>
vnoremap <leader>W :s/\s\+$//<CR>:let @/=''<CR>gv
endif
if g:chopsticks_space_keymaps
nnoremap <leader>fc :ChopsticksConfig<CR>
nnoremap <leader>fv :edit $MYVIMRC<CR>
nnoremap <leader>fV :ChopsticksReload<CR>
else
nnoremap <leader>ec :ChopsticksConfig<CR>
nnoremap <leader>ev :edit $MYVIMRC<CR>
nnoremap <leader>sv :ChopsticksReload<CR>
endif
if g:chopsticks_space_keymaps
nnoremap <leader>sr :%s/\<<C-r><C-w>\>//g<Left><Left>
vnoremap <leader>sr :s///g<Left><Left><Left>
else
nnoremap <leader>* :%s/\<<C-r><C-w>\>//g<Left><Left>
vnoremap <leader>* :s///g<Left><Left><Left>
endif
if has('clipboard')
if g:chopsticks_space_keymaps
nnoremap <leader>fp :let @+ = expand("%:p")<CR>:echo "Copied: " . expand("%:p")<CR>
nnoremap <leader>fn :let @+ = expand("%:t")<CR>:echo "Copied: " . expand("%:t")<CR>
else
nnoremap <leader>cp :let @+ = expand("%:p")<CR>:echo "Copied: " . expand("%:p")<CR>
nnoremap <leader>cf :let @+ = expand("%:t")<CR>:echo "Copied: " . expand("%:t")<CR>
endif
endif
if get(g:, 'chopsticks_enable_sudo_save_bang', 0)
cnoremap w!! w !sudo tee > /dev/null %
endif

28
scripts/test-common.sh Normal file
View file

@ -0,0 +1,28 @@
#!/usr/bin/env bash
# Shared setup for chopsticks test scripts.
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
TMP_ROOT="$(mktemp -d "${TMPDIR:-/tmp}/chopsticks-test-XXXXXX")"
EMPTY_XDG="$TMP_ROOT/xdg-empty"
STARTUP_LIMIT_MS="${STARTUP_LIMIT_MS:-150}"
cleanup() {
rm -rf "$TMP_ROOT"
}
trap cleanup EXIT
cd "$ROOT"
mkdir -p "$EMPTY_XDG"
step() {
printf '\n==> %s\n' "$1"
}
need() {
command -v "$1" >/dev/null 2>&1 || {
echo "Missing required command: $1" >&2
exit 1
}
}

144
scripts/test-quick.sh Executable file
View file

@ -0,0 +1,144 @@
#!/usr/bin/env bash
# Shell, docs, installer, and bootstrap checks.
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=scripts/test-common.sh
source "$SCRIPT_DIR/test-common.sh"
check_shell() {
step "Shell syntax and lint"
need bash
bash -n install.sh
bash -n get.sh
bash -n scripts/test.sh
bash -n scripts/test-common.sh
bash -n scripts/test-quick.sh
bash -n scripts/test-vim.sh
test -x install.sh
test -x get.sh
test -x scripts/test.sh
need shellcheck
shellcheck install.sh get.sh scripts/test.sh \
scripts/test-common.sh scripts/test-quick.sh scripts/test-vim.sh
}
check_docs() {
step "Markdown lint"
need markdownlint
markdownlint README.md QUICKSTART.md CONTRIBUTING.md CHANGELOG.md BETA.md
step "Documentation consistency"
for command in ChopsticksHelp ChopsticksConfig ChopsticksReload \
ChopsticksBeta ChopsticksBetaLog ChopsticksBetaSession
do
for file in README.md BETA.md doc/chopsticks.txt modules/cheatsheet.vim \
modules/tutor.vim modules/status.vim
do
grep -Fq "$command" "$file" || {
echo "Missing $command in $file" >&2
exit 1
}
done
done
grep -Fq '*chopsticks.txt*' doc/chopsticks.txt
grep -Fq '*chopsticks-space*' doc/chopsticks.txt
grep -Fq 'command! ChopsticksHelp' modules/help.vim
grep -Fq 'command! ChopsticksConfig' modules/utilities.vim
grep -Fq 'command! ChopsticksReload' modules/utilities.vim
for command in ChopsticksBeta ChopsticksBetaLog ChopsticksBetaSession; do
grep -Fq "command! $command" modules/beta.vim || {
echo "Missing $command definition in modules/beta.vim" >&2
exit 1
}
done
if command -v vhs >/dev/null 2>&1; then
vhs validate .github/demo.tape
else
echo "Skipping VHS tape validation: vhs not installed"
fi
}
check_installer_modes() {
step "Installer profile-only modes"
XDG_CONFIG_HOME="$TMP_ROOT/dry" ./install.sh --dry-run --profile=full \
| tee "$TMP_ROOT/install-dry-run.txt"
grep -q 'Profile: full' "$TMP_ROOT/install-dry-run.txt"
test ! -e "$TMP_ROOT/dry/chopsticks.vim"
XDG_CONFIG_HOME="$TMP_ROOT/config" ./install.sh --configure-only --profile=minimal
grep -q "let g:chopsticks_profile = 'minimal'" "$TMP_ROOT/config/chopsticks.vim"
XDG_CONFIG_HOME="$TMP_ROOT/config" ./install.sh --configure-only --profile=full
grep -q "let g:chopsticks_profile = 'full'" "$TMP_ROOT/config/chopsticks.vim"
XDG_CONFIG_HOME="$TMP_ROOT/default" ./install.sh --configure-only --yes
grep -q "let g:chopsticks_profile = 'engineer'" "$TMP_ROOT/default/chopsticks.vim"
}
check_bootstrap() {
step "Bootstrap dry-run safety"
CHOPSTICKS_DEST="$TMP_ROOT/bootstrap" ./get.sh --dry-run --profile=minimal \
| tee "$TMP_ROOT/get-dry-run.txt"
grep -q 'Would clone' "$TMP_ROOT/get-dry-run.txt"
test ! -e "$TMP_ROOT/bootstrap"
mkdir -p "$TMP_ROOT/no-git-bin"
printf '%s\n' \
'#!/usr/bin/env bash' \
"echo \"brew was called\" >> \"\$BREW_LOG\"" \
'exit 42' > "$TMP_ROOT/no-git-bin/brew"
chmod +x "$TMP_ROOT/no-git-bin/brew"
BREW_LOG="$TMP_ROOT/no-git-brew.log" \
PATH="$TMP_ROOT/no-git-bin" \
CHOPSTICKS_DEST="$TMP_ROOT/no-git-bootstrap" \
/bin/bash ./get.sh --dry-run --profile=full \
| tee "$TMP_ROOT/get-no-git-dry-run.txt"
grep -q 'Would require: git' "$TMP_ROOT/get-no-git-dry-run.txt"
grep -q 'Would clone' "$TMP_ROOT/get-no-git-dry-run.txt"
test ! -e "$TMP_ROOT/no-git-brew.log"
test ! -e "$TMP_ROOT/no-git-bootstrap"
mkdir -p "$TMP_ROOT/not-chopsticks"
git -c init.defaultBranch=main init "$TMP_ROOT/not-chopsticks" >/dev/null
git -C "$TMP_ROOT/not-chopsticks" remote add origin https://github.com/example/not-chopsticks.git
if CHOPSTICKS_DEST="$TMP_ROOT/not-chopsticks" ./get.sh --dry-run; then
echo "Expected get.sh to reject non-chopsticks repo" >&2
exit 1
fi
mkdir -p "$TMP_ROOT/chopsticks-existing"
git -c init.defaultBranch=main init "$TMP_ROOT/chopsticks-existing" >/dev/null
git -C "$TMP_ROOT/chopsticks-existing" remote add origin https://github.com/m1ngsama/chopsticks.git
touch "$TMP_ROOT/chopsticks-existing/install.sh" "$TMP_ROOT/chopsticks-existing/.vimrc"
CHOPSTICKS_DEST="$TMP_ROOT/chopsticks-existing" ./get.sh --dry-run --yes \
| tee "$TMP_ROOT/get-existing.txt"
grep -q 'Would update existing chopsticks repo' "$TMP_ROOT/get-existing.txt"
}
run_quick_group() {
case "$1" in
quick)
check_shell
check_docs
check_installer_modes
check_bootstrap
;;
shell) check_shell ;;
docs) check_docs ;;
installer) check_installer_modes ;;
bootstrap) check_bootstrap ;;
*)
echo "Unknown quick test group: $1" >&2
exit 1 ;;
esac
}
if [[ $# -eq 0 ]]; then
set -- quick
fi
for group in "$@"; do
run_quick_group "$group"
done

539
scripts/test-vim.sh Executable file
View file

@ -0,0 +1,539 @@
#!/usr/bin/env bash
# Vim smoke tests. Requires plugins in ~/.vim/plugged.
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=scripts/test-common.sh
source "$SCRIPT_DIR/test-common.sh"
check_plugin_dirs() {
step "Plugin directories"
for plugin in \
fzf fzf.vim vim-fugitive vim-gitgutter ale vim-lsp vim-lsp-settings \
asyncomplete.vim asyncomplete-lsp.vim vim-markdown
do
test -d "$HOME/.vim/plugged/$plugin" || {
echo "Missing plugin directory: $plugin" >&2
exit 1
}
done
}
check_vim() {
step "Vim smoke tests"
need vim
check_plugin_dirs
XDG_CONFIG_HOME="$EMPTY_XDG" vim -u .vimrc -i NONE -es -c 'qa!' 2>&1
XDG_CONFIG_HOME="$EMPTY_XDG" vim -u .vimrc -i NONE -es -N -c 'qa!' 2>&1
if [ -x /usr/bin/vim ] && [ "$(command -v vim)" != "/usr/bin/vim" ]; then
XDG_CONFIG_HOME="$EMPTY_XDG" /usr/bin/vim -u .vimrc -i NONE -es -N -c 'qa!' 2>&1
fi
XDG_CONFIG_HOME="$EMPTY_XDG" vim -u .vimrc -i NONE -es -N \
-c "redir! > $TMP_ROOT/plugs.txt" \
-c 'silent echo len(g:plugs)' \
-c 'redir END' \
-c 'qa!' 2>/dev/null
PLUGS="$(tr -d '[:space:]' < "$TMP_ROOT/plugs.txt")"
echo "Plugins registered: $PLUGS"
if [ "$PLUGS" -lt 20 ]; then
echo "Expected 20+ plugins, got $PLUGS" >&2
exit 1
fi
mkdir -p "$TMP_ROOT/chopsticks path/modules"
mkdir -p "$TMP_ROOT/chopsticks path/doc"
cp .vimrc "$TMP_ROOT/chopsticks path/.vimrc"
cp modules/*.vim "$TMP_ROOT/chopsticks path/modules/"
cp doc/*.txt "$TMP_ROOT/chopsticks path/doc/"
XDG_CONFIG_HOME="$EMPTY_XDG" vim -u "$TMP_ROOT/chopsticks path/.vimrc" \
-i NONE -es -N -c 'qa!' 2>&1
XDG_CONFIG_HOME="$EMPTY_XDG" \
vim -u "$TMP_ROOT/chopsticks path/.vimrc" -i NONE -es -N \
-c 'ChopsticksHelp' \
-c 'if expand("%:t") !=# "chopsticks.txt" | cquit | endif' \
-c 'if search("chopsticks-space", "n") == 0 | cquit | endif' \
-c 'qa!' 2>&1
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' \
-c 'qa!' 2>&1
mkdir -p "$TMP_ROOT/local"
printf "%s\n" "let g:chopsticks_profile = 'minimal'" > "$TMP_ROOT/local/config.vim"
vim -u NONE -i NONE -es -N \
-c "let g:chopsticks_local_config = '$TMP_ROOT/local/config.vim'" \
-c 'source .vimrc' \
-c 'if g:chopsticks_resolved_local_config !~# "config.vim$" || g:chopsticks_profile !=# "minimal" || has_key(g:plugs, "ale") || has_key(g:plugs, "vim-lsp") || has_key(g:plugs, "auto-pairs") | cquit | endif' \
-c 'qa!' 2>&1
mkdir -p "$TMP_ROOT/xdg"
printf "%s\n" "let g:chopsticks_profile = 'minimal'" > "$TMP_ROOT/xdg/chopsticks.vim"
XDG_CONFIG_HOME="$TMP_ROOT/xdg" vim -u NONE -i NONE -es -N \
-c 'source .vimrc' \
-c 'if g:chopsticks_resolved_local_config !~# "chopsticks.vim$" || g:chopsticks_profile !=# "minimal" || has_key(g:plugs, "ale") || has_key(g:plugs, "vim-lsp") || has_key(g:plugs, "auto-pairs") | cquit | endif' \
-c 'qa!' 2>&1
local_config_cmd="$TMP_ROOT/config command/chopsticks.vim"
vim -u NONE -i NONE -es -N \
-c "let g:chopsticks_local_config = '$local_config_cmd'" \
-c 'source .vimrc' \
-c 'ChopsticksConfig' \
-c 'if expand("%:p") !=# g:chopsticks_resolved_local_config || &l:filetype !=# "vim" | cquit | endif' \
-c 'if getline(1) !~# "chopsticks local preferences" || &modified | cquit | endif' \
-c 'qa!' 2>&1
test -d "$(dirname "$local_config_cmd")"
test ! -e "$local_config_cmd"
XDG_CONFIG_HOME="$EMPTY_XDG" vim -u .vimrc -i NONE -es -N \
-c 'ChopsticksStatus' \
-c "redir! > $TMP_ROOT/status-default.txt" \
-c 'silent %print' \
-c 'redir END' \
-c 'qa!' 2>&1
if grep -Fq 'vim-lsp not loaded' "$TMP_ROOT/status-default.txt"; then
cat "$TMP_ROOT/status-default.txt"
exit 1
fi
grep -Fq 'OK vim-lsp stack (installed)' "$TMP_ROOT/status-default.txt"
grep -Fq 'help :ChopsticksHelp :ChopsticksTutor SPC ?' "$TMP_ROOT/status-default.txt"
grep -Fq 'commands :ChopsticksConfig :ChopsticksReload' "$TMP_ROOT/status-default.txt"
grep -Fq 'candidate 2.3.0' "$TMP_ROOT/status-default.txt"
grep -Fq 'keymap space' "$TMP_ROOT/status-default.txt"
grep -Fq 'commands :ChopsticksBeta :ChopsticksBetaLog' "$TMP_ROOT/status-default.txt"
grep -Fq ':ChopsticksBetaSession' "$TMP_ROOT/status-default.txt"
grep -Fq 'chopsticks-2.3.0.md' "$TMP_ROOT/status-default.txt"
grep -Fq 'python (:LspInstallServer in a python file)' "$TMP_ROOT/status-default.txt"
grep -Fq 'LSP actions are buffer-local and start after a server attaches.' "$TMP_ROOT/status-default.txt"
grep -Fq 'Open that filetype and run :LspInstallServer once.' "$TMP_ROOT/status-default.txt"
XDG_CONFIG_HOME="$EMPTY_XDG" vim -u .vimrc -i NONE -es -N \
-c 'let last_change_map = nr2char(96) . "[v" . nr2char(96) . "]"' \
-c 'if maparg("0", "n") !=# "" || maparg("0", "v") !=# "" || maparg("Y", "n") !=# "" || maparg("Q", "n") !=# "" || maparg("<Space>", "n") !=# "" || maparg("//", "v") !=# "" || maparg("gV", "n") !=# "" || maparg("jk", "i") !=# "" || maparg("<C-s>", "n") !=# "" || maparg("<C-s>", "i") !=# "" || maparg("<C-p>", "n") !=# "" || maparg("<C-p>", "c") !=# "" || maparg("<C-n>", "c") !=# "" || maparg("w!!", "c") !=# "" | cquit | endif' \
-c 'if maparg("<C-h>", "n") !~# "NavigateWindow" || maparg("<C-j>", "n") !~# "NavigateWindow" || maparg("<C-k>", "n") !~# "NavigateWindow" || maparg("<C-l>", "n") !~# "NavigateWindow" | cquit | endif' \
-c 'if has_key(g:plugs, "auto-pairs") || maparg("<Tab>", "i") =~# "pumvisible" || maparg("<S-Tab>", "i") =~# "pumvisible" || maparg("<CR>", "i") =~# "asyncomplete#close_popup" || maparg("<CR>", "i") =~# "AutoPairs" | cquit | endif' \
-c 'if maparg("<Esc><Esc>", "t") !=# "" || maparg("<C-h>", "t") !=# "" || maparg("<C-j>", "t") !=# "" || maparg("<C-k>", "t") !=# "" || maparg("<C-l>", "t") !=# "" | cquit | endif' \
-c 'if maparg("s", "n") !~# "easymotion-overwin-f2" | cquit | endif' \
-c 'if maparg("<Space>/", "v") !~# "escape" || maparg("<Space>v", "n") !=# last_change_map || maparg("<Space><Space>", "n") !~# "SmartFiles" | cquit | endif' \
-c 'if maparg(",/", "v") !=# "" || maparg(",v", "n") !=# "" || maparg(",ff", "n") !=# "" | cquit | endif' \
-c 'qa!' 2>&1
XDG_CONFIG_HOME="$EMPTY_XDG" vim -u NONE -i NONE -es -N \
-c 'let g:chopsticks_keymap_style = "classic"' \
-c 'source .vimrc' \
-c 'let last_change_map = nr2char(96) . "[v" . nr2char(96) . "]"' \
-c 'if mapleader !=# "," || maparg("s", "n") !=# "" || maparg(",/", "v") !~# "escape" || maparg(",v", "n") !=# last_change_map || maparg(",ff", "n") !~# "SmartFiles" | cquit | endif' \
-c 'if maparg("<C-h>", "n") !~# "NavigateWindow" || maparg("<C-j>", "n") !~# "NavigateWindow" || maparg("<C-k>", "n") !~# "NavigateWindow" || maparg("<C-l>", "n") !~# "NavigateWindow" | cquit | endif' \
-c 'if maparg(",ec", "n") !~# "ChopsticksConfig" || maparg(",sv", "n") !~# "ChopsticksReload" | cquit | endif' \
-c 'if maparg(",gp", "n") !=# "" || maparg(",gl", "n") !=# "" | cquit | endif' \
-c 'qa!' 2>&1
XDG_CONFIG_HOME="$EMPTY_XDG" vim -u NONE -i NONE -es -N \
-c 'let g:chopsticks_enable_jk_escape = 1' \
-c 'source .vimrc' \
-c 'if maparg("jk", "i") !~# "<Esc>" | cquit | endif' \
-c 'qa!' 2>&1
XDG_CONFIG_HOME="$EMPTY_XDG" vim -u NONE -i NONE -es -N \
-c 'let g:chopsticks_enable_ctrl_s_save = 1' \
-c 'let g:chopsticks_enable_sudo_save_bang = 1' \
-c 'let g:chopsticks_enable_completion_keymaps = 1' \
-c 'source .vimrc' \
-c 'if maparg("<C-s>", "n") !~# ":w" || maparg("<C-s>", "i") !~# ":w" || maparg("w!!", "c") !~# "sudo tee" | cquit | endif' \
-c 'if maparg("<Tab>", "i") !~# "pumvisible" || maparg("<S-Tab>", "i") !~# "pumvisible" || maparg("<CR>", "i") !~# "asyncomplete#close_popup" | cquit | endif' \
-c 'qa!' 2>&1
XDG_CONFIG_HOME="$EMPTY_XDG" vim -u NONE -i NONE -es -N \
-c 'let g:chopsticks_enable_auto_pairs = 1' \
-c 'source .vimrc' \
-c 'if !has_key(g:plugs, "auto-pairs") | cquit | endif' \
-c 'qa!' 2>&1
XDG_CONFIG_HOME="$EMPTY_XDG" vim -u NONE -i NONE -es -N \
-c 'let g:chopsticks_enable_terminal_keymaps = 1' \
-c 'source .vimrc' \
-c 'if has("terminal") && (maparg("<Esc><Esc>", "t") !~# "<C-\\\\><C-N>" || maparg("<C-h>", "t") !~# "NavigateWindow" || maparg("<C-j>", "t") !~# "NavigateWindow" || maparg("<C-k>", "t") !~# "NavigateWindow" || maparg("<C-l>", "t") !~# "NavigateWindow") | cquit | endif' \
-c 'qa!' 2>&1
TMUX=/tmp/chopsticks-test XDG_CONFIG_HOME="$EMPTY_XDG" vim -u .vimrc -i NONE -es -N \
-c 'if has_key(g:plugs, "vim-tmux-navigator") | cquit | endif' \
-c 'qa!' 2>&1
TMUX=/tmp/chopsticks-test XDG_CONFIG_HOME="$EMPTY_XDG" vim -u NONE -i NONE -es -N \
-c 'let g:chopsticks_enable_tmux_navigator = 1' \
-c 'source .vimrc' \
-c 'if !has_key(g:plugs, "vim-tmux-navigator") | cquit | endif' \
-c 'qa!' 2>&1
XDG_CONFIG_HOME="$EMPTY_XDG" vim -u .vimrc -i NONE -es -N \
-c 'setfiletype netrw' \
-c 'if &filetype !=# "netrw" | cquit | endif' \
-c 'if !maparg("<C-l>", "n", 0, 1).buffer | cquit | endif' \
-c 'if maparg("<C-h>", "n") !~# "NavigateWindow" || maparg("<C-l>", "n") !~# "NavigateWindow" | cquit | endif' \
-c 'qa!' 2>&1
XDG_CONFIG_HOME="$EMPTY_XDG" vim -u .vimrc -i NONE -es -N \
-c 'if &exrc || &secure | cquit | endif' \
-c 'qa!' 2>&1
XDG_CONFIG_HOME="$EMPTY_XDG" vim -u NONE -i NONE -es -N \
-c 'let g:chopsticks_enable_exrc = 1' \
-c 'source .vimrc' \
-c 'if !&exrc || !&secure | cquit | endif' \
-c 'qa!' 2>&1
XDG_CONFIG_HOME="$EMPTY_XDG" vim -u .vimrc -i NONE -es -N \
-c 'if maparg("<Space>c=", "n") !=# "" | cquit | endif' \
-c 'if maparg("<Space>=", "v") !~# "=" | cquit | endif' \
-c 'qa!' 2>&1
XDG_CONFIG_HOME="$EMPTY_XDG" vim -u NONE -i NONE -es -N \
-c 'let g:chopsticks_enable_reindent_file = 1' \
-c 'source .vimrc' \
-c 'if maparg("<Space>c=", "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' \
-c 'ChopsticksStatus' \
-c "redir! > $TMP_ROOT/status-lsp-not-loaded.txt" \
-c 'silent %print' \
-c 'redir END' \
-c 'qa!' 2>&1
grep -Fq 'OK vim-lsp stack (installed; not loaded yet)' "$TMP_ROOT/status-lsp-not-loaded.txt"
XDG_CONFIG_HOME="$EMPTY_XDG" vim -u NONE -i NONE -es -N \
-c 'let g:chopsticks_profile = "minimal"' \
-c 'source .vimrc' \
-c 'ChopsticksStatus' \
-c "redir! > $TMP_ROOT/status-minimal.txt" \
-c 'silent %print' \
-c 'redir END' \
-c 'qa!' 2>&1
grep -Fq 'off vim-lsp stack (LSP disabled by profile)' "$TMP_ROOT/status-minimal.txt"
grep -Fq 'off python (LSP disabled by profile)' "$TMP_ROOT/status-minimal.txt"
if grep -Fq 'LSP actions are buffer-local' "$TMP_ROOT/status-minimal.txt"; then
cat "$TMP_ROOT/status-minimal.txt"
exit 1
fi
mkdir -p "$TMP_ROOT/missing-home/.vim/autoload"
cp "$HOME/.vim/autoload/plug.vim" "$TMP_ROOT/missing-home/.vim/autoload/plug.vim"
HOME="$TMP_ROOT/missing-home" XDG_CONFIG_HOME="$EMPTY_XDG" \
vim -u .vimrc -i NONE -es -N \
-c 'ChopsticksStatus' \
-c "redir! > $TMP_ROOT/status-missing-plugin.txt" \
-c 'silent %print' \
-c 'redir END' \
-c 'qa!' 2>&1
grep -Fq 'vim-lsp not installed; run :PlugInstall' "$TMP_ROOT/status-missing-plugin.txt"
XDG_CONFIG_HOME="$EMPTY_XDG" vim -u .vimrc -i NONE -es -N \
-c 'doautocmd User lsp_buffer_enabled' \
-c 'if maparg("gd", "n") !~# "lsp-definition" || maparg("gr", "n") !~# "lsp-references" || maparg("gI", "n") !~# "lsp-implementation" || maparg("gy", "n") !~# "lsp-type-definition" || maparg("K", "n") !~# "lsp-hover" | cquit | endif' \
-c 'if maparg("[d", "n") !~# "lsp-previous-diagnostic" || maparg("]d", "n") !~# "lsp-next-diagnostic" | cquit | endif' \
-c 'if maparg("<Space>ca", "n") !~# "lsp-code-action" || maparg("<Space>cr", "n") !~# "lsp-rename" || maparg("<Space>cf", "n") !~# "lsp-document-format" | cquit | endif' \
-c 'if maparg("<Space>ci", "n") !~# "LspStatus" || maparg("<Space>co", "n") !~# "lsp-document-symbol-search" | cquit | endif' \
-c 'if maparg("<Space>cd", "n") !=# "" || maparg("<Space>ck", "n") !=# "" || maparg("<Space>cp", "n") !=# "" || maparg("<Space>cn", "n") !=# "" | cquit | endif' \
-c 'qa!' 2>&1
XDG_CONFIG_HOME="$EMPTY_XDG" vim -u NONE -i NONE -es -N \
-c 'let g:chopsticks_keymap_style = "classic"' \
-c 'source .vimrc' \
-c 'doautocmd User lsp_buffer_enabled' \
-c 'if maparg("gd", "n") !=# "" || maparg("K", "n") !=# "" || maparg("gI", "n") !=# "" || maparg("gr", "n") !=# "" | cquit | endif' \
-c 'if maparg(",dd", "n") !~# "lsp-definition" || maparg(",dt", "n") !~# "lsp-type-definition" || maparg(",di", "n") !~# "lsp-implementation" || maparg(",dr", "n") !~# "lsp-references" || maparg(",dk", "n") !~# "lsp-hover" | cquit | endif' \
-c 'if maparg(",dp", "n") !~# "lsp-previous-diagnostic" | cquit | endif' \
-c 'if maparg(",dn", "n") !~# "lsp-next-diagnostic" | cquit | endif' \
-c 'qa!' 2>&1
XDG_CONFIG_HOME="$EMPTY_XDG" vim -u NONE -i NONE -es -N \
-c 'let g:chopsticks_keymap_style = "space"' \
-c 'source .vimrc' \
-c 'if mapleader !=# "\<Space>" || maplocalleader !=# "," | cquit | endif' \
-c 'if maparg(",ff", "n") !=# "" || maparg(",w", "n") !=# "" || maparg(",mt", "n") !=# "" || maparg(",gp", "n") !=# "" || maparg("<Space>gp", "n") !=# "" | cquit | endif' \
-c 'if maparg("<Space>f", "n") !=# "" || maparg("<Space>u", "n") !=# "" || maparg("<Space>c", "n") !=# "" || maparg("<Space>x", "n") !=# "" || maparg("<Space>wm", "n") !=# "" || maparg("<Space>w+", "n") !=# "" || maparg("<Space>w-", "n") !=# "" | cquit | endif' \
-c 'if maparg("<Space><Space>", "n") !~# "SmartFiles" || maparg("<Space>ff", "n") !~# "SmartFiles" || maparg("<Space>,", "n") !~# "Buffers" || maparg("<Space>bd", "n") !~# "Bclose" | cquit | endif' \
-c 'if maparg("<Space>w", "n") !~# ":w" || maparg("<Space>W", "n") !~# ":wa" || maparg("<Space>q", "n") !~# ":q" || maparg("<Space>qq", "n") !=# "" || maparg("<Space>qx", "n") !=# "" || maparg("<Space>fc", "n") !~# "ChopsticksConfig" || maparg("<Space>fV", "n") !~# "ChopsticksReload" || maparg("<Space>U", "n") !~# "UndotreeToggle" || maparg("<Space>fs", "n") !=# "" || maparg("<Space>bu", "n") !=# "" | cquit | endif' \
-c 'if maparg("<Space>gl", "n") !~# "Git log" || maparg("<Space>gC", "n") !~# "Commits" | cquit | endif' \
-c 'qa!' 2>&1
XDG_CONFIG_HOME="$EMPTY_XDG" vim -u NONE -i NONE -es -N \
-c 'let g:chopsticks_keymap_style = "space"' \
-c 'source .vimrc' \
-c 'doautocmd User lsp_buffer_enabled' \
-c 'if maparg("<Space>cf", "n") !~# "lsp-document-format" | cquit | endif' \
-c 'if maparg("gd", "n") !~# "lsp-definition" || maparg("gr", "n") !~# "lsp-references" || maparg("K", "n") !~# "lsp-hover" | cquit | endif' \
-c 'if maparg("<Space>cd", "n") !=# "" || maparg("<Space>ck", "n") !=# "" | cquit | endif' \
-c 'if maparg("<Space>f", "n") !=# "" || maparg("<Space>c", "n") !=# "" | cquit | endif' \
-c 'if maparg(",f", "n") !=# "" || maparg(",dd", "n") !=# "" | cquit | endif' \
-c 'qa!' 2>&1
XDG_CONFIG_HOME="$EMPTY_XDG" vim -u .vimrc -i NONE -es -N \
-c 'call feedkeys("\<Space>?", "xt")' \
-c 'if max(map(getline(1, "$"), "strdisplaywidth(v:val)")) > 42 | cquit | endif' \
-c "redir! > $TMP_ROOT/cheat-default.txt" \
-c 'silent %print' \
-c 'redir END' \
-c 'qa!' 2>&1
grep -Fq ':ChopsticksStatus check LSP setup' "$TMP_ROOT/cheat-default.txt"
grep -Fq 'trained loop:' "$TMP_ROOT/cheat-default.txt"
grep -Fq 'files → s jump → gd/K' "$TMP_ROOT/cheat-default.txt"
grep -Fq 'run → grep → git' "$TMP_ROOT/cheat-default.txt"
grep -Fq 'SPC SPC files' "$TMP_ROOT/cheat-default.txt"
grep -Fq 'gd definition' "$TMP_ROOT/cheat-default.txt"
grep -Fq 'K hover docs' "$TMP_ROOT/cheat-default.txt"
grep -Fq '[d ]d LSP diagnostics' "$TMP_ROOT/cheat-default.txt"
grep -Fq 'Ctrl-hjkl windows' "$TMP_ROOT/cheat-default.txt"
grep -Fq '<C-w>hjkl native fallback' "$TMP_ROOT/cheat-default.txt"
grep -Fq 'SPC w save' "$TMP_ROOT/cheat-default.txt"
grep -Fq 'SPC fc edit local config' "$TMP_ROOT/cheat-default.txt"
grep -Fq 's+2ch easymotion jump' "$TMP_ROOT/cheat-default.txt"
grep -Fq 'cl / cc native s / S substitute' "$TMP_ROOT/cheat-default.txt"
grep -Fq ':ChopsticksHelp full help' "$TMP_ROOT/cheat-default.txt"
grep -Fq ':ChopsticksConfig local config' "$TMP_ROOT/cheat-default.txt"
grep -Fq ':ChopsticksReload reload config' "$TMP_ROOT/cheat-default.txt"
grep -Fq ':ChopsticksTutor practice' "$TMP_ROOT/cheat-default.txt"
grep -Fq ':ChopsticksBeta release checklist' "$TMP_ROOT/cheat-default.txt"
grep -Fq ':ChopsticksBetaLog release notes' "$TMP_ROOT/cheat-default.txt"
grep -Fq ':ChopsticksBetaSession new release note' "$TMP_ROOT/cheat-default.txt"
if grep -Eq 'Ctrl\\+p find file|Ctrl\\+hjkl navigate splits|Ctrl\\+s save|jk exit insert|SPC fs save|SPC cd definition|SPC ck hover|SPC wm|SPC w\\+/-|\\[g \\]g LSP diagnostics' "$TMP_ROOT/cheat-default.txt"; then
cat "$TMP_ROOT/cheat-default.txt"
exit 1
fi
XDG_CONFIG_HOME="$EMPTY_XDG" vim -u .vimrc -i NONE -es -N \
-c 'ChopsticksCheatSheet' \
-c 'if max(map(getline(1, "$"), "strdisplaywidth(v:val)")) > 42 | cquit | endif' \
-c "redir! > $TMP_ROOT/cheat-command.txt" \
-c 'silent %print' \
-c 'redir END' \
-c 'qa!' 2>&1
grep -Fq 'SPC SPC files' "$TMP_ROOT/cheat-command.txt"
grep -Fq ':ChopsticksTutor practice' "$TMP_ROOT/cheat-command.txt"
XDG_CONFIG_HOME="$EMPTY_XDG" vim -u NONE -i NONE -es -N \
-c 'let g:chopsticks_keymap_style = "classic"' \
-c 'source .vimrc' \
-c 'normal ,?' \
-c 'if max(map(getline(1, "$"), "strdisplaywidth(v:val)")) > 42 | cquit | endif' \
-c "redir! > $TMP_ROOT/cheat-classic.txt" \
-c 'silent %print' \
-c 'redir END' \
-c 'qa!' 2>&1
grep -Fq ',ff files' "$TMP_ROOT/cheat-classic.txt"
grep -Fq 'trained loop:' "$TMP_ROOT/cheat-classic.txt"
grep -Fq 'files → jump → inspect' "$TMP_ROOT/cheat-classic.txt"
grep -Fq 'run → grep → git' "$TMP_ROOT/cheat-classic.txt"
grep -Fq ',dd definition' "$TMP_ROOT/cheat-classic.txt"
grep -Fq ',dk hover docs' "$TMP_ROOT/cheat-classic.txt"
grep -Fq ',dp ,dn LSP diagnostics' "$TMP_ROOT/cheat-classic.txt"
grep -Fq ',ec edit local config' "$TMP_ROOT/cheat-classic.txt"
grep -Fq ':ChopsticksConfig local config' "$TMP_ROOT/cheat-classic.txt"
if grep -Eq ',gp push|,gl pull' "$TMP_ROOT/cheat-classic.txt"; then
cat "$TMP_ROOT/cheat-classic.txt"
exit 1
fi
XDG_CONFIG_HOME="$EMPTY_XDG" vim -u NONE -i NONE -es -N \
-c 'let g:chopsticks_profile = "minimal"' \
-c 'source .vimrc' \
-c 'call feedkeys("\<Space>?", "xt")' \
-c 'if max(map(getline(1, "$"), "strdisplaywidth(v:val)")) > 42 | cquit | endif' \
-c "redir! > $TMP_ROOT/cheat.txt" \
-c 'silent %print' \
-c 'redir END' \
-c 'qa!' 2>&1
if grep -Eq 'definition|LspInstallServer|ALE errors|undo tree|markdown preview' "$TMP_ROOT/cheat.txt"; then
cat "$TMP_ROOT/cheat.txt"
exit 1
fi
grep -q 'SPC rr run file' "$TMP_ROOT/cheat.txt"
XDG_CONFIG_HOME="$EMPTY_XDG" vim -u NONE -i NONE -es -N \
-c 'let g:chopsticks_keymap_style = "space"' \
-c 'source .vimrc' \
-c 'call feedkeys("\<Space>?", "xt")' \
-c 'if max(map(getline(1, "$"), "strdisplaywidth(v:val)")) > 42 | cquit | endif' \
-c "redir! > $TMP_ROOT/cheat-space.txt" \
-c 'silent %print' \
-c 'redir END' \
-c 'qa!' 2>&1
grep -Fq 'SPC w save' "$TMP_ROOT/cheat-space.txt"
grep -Fq 'gd definition' "$TMP_ROOT/cheat-space.txt"
grep -Fq 'SPC gl log graph' "$TMP_ROOT/cheat-space.txt"
grep -Fq 'SPC fc edit local config' "$TMP_ROOT/cheat-space.txt"
grep -Fq 's+2ch easymotion jump' "$TMP_ROOT/cheat-space.txt"
if grep -Eq ',w save|,gp push|SPC gp push|SPC gl pull|SPC fs save|SPC cd definition|SPC f format' "$TMP_ROOT/cheat-space.txt"; then
cat "$TMP_ROOT/cheat-space.txt"
exit 1
fi
XDG_CONFIG_HOME="$EMPTY_XDG" vim -u .vimrc -i NONE -es -N \
-c 'ChopsticksTutor' \
-c 'if max(map(getline(1, "$"), "strdisplaywidth(v:val)")) > 78 | cquit | endif' \
-c "redir! > $TMP_ROOT/tutor-default.txt" \
-c 'silent %print' \
-c 'redir END' \
-c 'qa!' 2>&1
grep -Fq 'chopsticks tutor' "$TMP_ROOT/tutor-default.txt"
grep -Fq 'Goal: train one long-term project loop around Vim.' "$TMP_ROOT/tutor-default.txt"
grep -Fq '1. trained loop' "$TMP_ROOT/tutor-default.txt"
grep -Fq 'SPC ? active cheat sheet' "$TMP_ROOT/tutor-default.txt"
grep -Fq 'SPC fc edit local config' "$TMP_ROOT/tutor-default.txt"
grep -Fq ':ChopsticksHelp full help' "$TMP_ROOT/tutor-default.txt"
grep -Fq ':ChopsticksConfig local config' "$TMP_ROOT/tutor-default.txt"
grep -Fq 'Ctrl-h/j/k/l split navigation' "$TMP_ROOT/tutor-default.txt"
grep -Fq 'SPC e, Ctrl-h/l enter/leave sidebar' "$TMP_ROOT/tutor-default.txt"
grep -Fq 's + 2 chars visible jump' "$TMP_ROOT/tutor-default.txt"
grep -Fq 'cl / cc native s / S substitute' "$TMP_ROOT/tutor-default.txt"
grep -Fq 'gd / gr / K definition / refs / docs' "$TMP_ROOT/tutor-default.txt"
XDG_CONFIG_HOME="$EMPTY_XDG" vim -u NONE -i NONE -es -N \
-c 'let g:chopsticks_keymap_style = "classic"' \
-c 'source .vimrc' \
-c 'ChopsticksTutor' \
-c 'if max(map(getline(1, "$"), "strdisplaywidth(v:val)")) > 78 | cquit | endif' \
-c "redir! > $TMP_ROOT/tutor-classic.txt" \
-c 'silent %print' \
-c 'redir END' \
-c 'qa!' 2>&1
grep -Fq 'classic layout' "$TMP_ROOT/tutor-classic.txt"
grep -Fq 'Goal: train one long-term project loop around Vim.' "$TMP_ROOT/tutor-classic.txt"
grep -Fq ',? active cheat sheet' "$TMP_ROOT/tutor-classic.txt"
grep -Fq ',ec edit local config' "$TMP_ROOT/tutor-classic.txt"
grep -Fq 'Ctrl-h/j/k/l split navigation' "$TMP_ROOT/tutor-classic.txt"
grep -Fq ',S + 2 chars EasyMotion jump' "$TMP_ROOT/tutor-classic.txt"
XDG_CONFIG_HOME="$EMPTY_XDG" vim -u .vimrc -i NONE -es -N \
-c 'ChopsticksBeta' \
-c 'if max(map(getline(1, "$"), "strdisplaywidth(v:val)")) > 78 | cquit | endif' \
-c "redir! > $TMP_ROOT/beta-guide.txt" \
-c 'silent %print' \
-c 'redir END' \
-c 'qa!' 2>&1
grep -Fq 'chopsticks 2.3.0' "$TMP_ROOT/beta-guide.txt"
grep -Fq 'Prove this can be the long-term project loop.' "$TMP_ROOT/beta-guide.txt"
grep -Fq 'Record real editing friction before release.' "$TMP_ROOT/beta-guide.txt"
grep -Fq 'no private wiki is needed to remember the daily loop' "$TMP_ROOT/beta-guide.txt"
grep -Fq 'window/sidebar navigation beats native <C-w> only' "$TMP_ROOT/beta-guide.txt"
grep -Fq 'SPC ? active cheat sheet' "$TMP_ROOT/beta-guide.txt"
grep -Fq 'BETA.md release checklist and rollback' "$TMP_ROOT/beta-guide.txt"
grep -Fq ':ChopsticksBetaLog editable local release notes' "$TMP_ROOT/beta-guide.txt"
grep -Fq ':ChopsticksBetaSession append a new session block' "$TMP_ROOT/beta-guide.txt"
beta_log="$TMP_ROOT/release log/chopsticks-2.3.0.md"
XDG_CONFIG_HOME="$EMPTY_XDG" vim -u NONE -i NONE -es -N \
-c "let g:chopsticks_beta_log = '$beta_log'" \
-c 'source .vimrc' \
-c 'ChopsticksBetaLog' \
-c 'if expand("%:p") !~# "chopsticks-2.3.0.md" || &l:filetype !=# "markdown" | cquit | endif' \
-c 'qa!' 2>&1
grep -Fq '# chopsticks 2.3.0 release log' "$beta_log"
grep -Fq 'First key tried when stuck:' "$beta_log"
printf '%s\n' '- keep-existing-note' >> "$beta_log"
XDG_CONFIG_HOME="$EMPTY_XDG" vim -u NONE -i NONE -es -N \
-c "let g:chopsticks_beta_log = '$beta_log'" \
-c 'source .vimrc' \
-c 'ChopsticksBetaLog' \
-c 'qa!' 2>&1
grep -Fq -- '- keep-existing-note' "$beta_log"
before_sessions="$(grep -c '^## ' "$beta_log")"
XDG_CONFIG_HOME="$EMPTY_XDG" vim -u NONE -i NONE -es -N \
-c "let g:chopsticks_beta_log = '$beta_log'" \
-c 'source .vimrc' \
-c 'ChopsticksBetaSession' \
-c 'qa!' 2>&1
after_sessions="$(grep -c '^## ' "$beta_log")"
test "$after_sessions" -eq $((before_sessions + 1))
grep -Fq -- '- keep-existing-note' "$beta_log"
XDG_CONFIG_HOME="$EMPTY_XDG" vim -u NONE -i NONE -es -N README.md \
-c 'let g:chopsticks_keymap_style = "space"' \
-c 'source .vimrc' \
-c 'set filetype=markdown' \
-c 'if maparg(",mt", "n") !~# "Toc" || maparg(",mp", "n") !~# "PrevimOpen" | cquit | endif' \
-c 'qa!' 2>&1
XDG_CONFIG_HOME="$EMPTY_XDG" vim -u .vimrc -i NONE -es -N README.md \
-c 'set filetype=markdown' \
-c 'if &l:spell || &l:conceallevel != 0 || &l:signcolumn !=# "no" || exists("g:lsp_settings_filetype_markdown") | cquit | endif' \
-c 'qa!' 2>&1
XDG_CONFIG_HOME="$EMPTY_XDG" vim -u .vimrc -i NONE -es -N \
-c 'if maparg("s", "n") !~# "easymotion-overwin-f2" | cquit | endif' \
-c 'if maparg("<Space>w", "n") =~# "!" | cquit | endif' \
-c 'if !&swapfile || !&writebackup || &directory !~# "\.vim/.swap" | cquit | endif' \
-c 'qa!' 2>&1
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' \
-c 'qa!' 2>&1
truncate -s 11000000 "$TMP_ROOT/large.py"
XDG_CONFIG_HOME="$EMPTY_XDG" vim -u .vimrc -i NONE -es -N "$TMP_ROOT/large.py" \
-c 'set filetype=python' \
-c 'if &l:syntax !=# "" || &l:undolevels != -1 || &l:swapfile || get(b:, "ale_enabled", 1) != 0 | cquit | endif' \
-c 'qa!' 2>&1
mkdir -p "$TMP_ROOT/fake-bin" "$TMP_ROOT/c runner"
cat > "$TMP_ROOT/fake-bin/gcc" <<'GCCEOF'
#!/usr/bin/env bash
set -eu
printf '%s\n' "$@" > "$GCC_ARGS"
out=""
while [ "$#" -gt 0 ]; do
if [ "$1" = "-o" ]; then
shift
out="$1"
fi
shift || true
done
test -n "$out"
printf '%s\n' '#!/usr/bin/env bash' 'exit 0' > "$out"
chmod +x "$out"
GCCEOF
chmod +x "$TMP_ROOT/fake-bin/gcc"
c_file="$TMP_ROOT/c runner/main.c"
c_file_real="$(cd "$TMP_ROOT/c runner" && pwd -P)/main.c"
printf '%s\n' 'int main(void) { return 0; }' > "$c_file"
GCC_ARGS="$TMP_ROOT/gcc-args.txt" \
PATH="$TMP_ROOT/fake-bin:$PATH" \
XDG_CONFIG_HOME="$EMPTY_XDG" \
vim -u .vimrc -i NONE -es -N "$c_file" \
-c 'set filetype=c' \
-c 'call feedkeys("\<Space>rr", "xt")' \
-c 'qa!' 2>&1
c_out="$(sed -n '2p' "$TMP_ROOT/gcc-args.txt")"
test -n "$c_out"
test "$c_out" != "/tmp/a.out"
test ! -e "$c_out"
grep -Fxq "$c_file_real" "$TMP_ROOT/gcc-args.txt"
XDG_CONFIG_HOME="$EMPTY_XDG" vim -u .vimrc -i NONE --startuptime "$TMP_ROOT/startup.log" \
-es -N -c 'qa!' 2>/dev/null
tail -1 "$TMP_ROOT/startup.log"
STARTUP_MS="$(awk 'END { print $1 }' "$TMP_ROOT/startup.log")"
awk -v ms="$STARTUP_MS" -v limit="$STARTUP_LIMIT_MS" \
'BEGIN { if (ms > limit) exit 1 }'
}
check_vim

View file

@ -1,31 +1,9 @@
#!/usr/bin/env bash
# Project test runner. CI calls the same groups that maintainers can run locally.
# Project test runner. CI calls the same groups maintainers can run locally.
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
TMP_ROOT="$(mktemp -d "${TMPDIR:-/tmp}/chopsticks-test-XXXXXX")"
EMPTY_XDG="$TMP_ROOT/xdg-empty"
STARTUP_LIMIT_MS="${STARTUP_LIMIT_MS:-150}"
cleanup() {
rm -rf "$TMP_ROOT"
}
trap cleanup EXIT
cd "$ROOT"
mkdir -p "$EMPTY_XDG"
step() {
printf '\n==> %s\n' "$1"
}
need() {
command -v "$1" >/dev/null 2>&1 || {
echo "Missing required command: $1" >&2
exit 1
}
}
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
usage() {
cat <<'EOF'
@ -50,385 +28,17 @@ list_groups() {
printf '%s\n' quick shell docs installer bootstrap vim all
}
check_shell() {
step "Shell syntax and lint"
need bash
bash -n install.sh
bash -n get.sh
bash -n scripts/test.sh
test -x install.sh
test -x get.sh
test -x scripts/test.sh
need shellcheck
shellcheck install.sh get.sh scripts/test.sh
}
check_docs() {
step "Markdown lint"
need markdownlint
markdownlint README.md QUICKSTART.md CONTRIBUTING.md CHANGELOG.md
}
check_installer_modes() {
step "Installer profile-only modes"
XDG_CONFIG_HOME="$TMP_ROOT/dry" ./install.sh --dry-run --profile=full \
| tee "$TMP_ROOT/install-dry-run.txt"
grep -q 'Profile: full' "$TMP_ROOT/install-dry-run.txt"
test ! -e "$TMP_ROOT/dry/chopsticks.vim"
XDG_CONFIG_HOME="$TMP_ROOT/config" ./install.sh --configure-only --profile=minimal
grep -q "let g:chopsticks_profile = 'minimal'" "$TMP_ROOT/config/chopsticks.vim"
XDG_CONFIG_HOME="$TMP_ROOT/config" ./install.sh --configure-only --profile=full
grep -q "let g:chopsticks_profile = 'full'" "$TMP_ROOT/config/chopsticks.vim"
XDG_CONFIG_HOME="$TMP_ROOT/default" ./install.sh --configure-only --yes
grep -q "let g:chopsticks_profile = 'engineer'" "$TMP_ROOT/default/chopsticks.vim"
}
check_bootstrap() {
step "Bootstrap dry-run safety"
CHOPSTICKS_DEST="$TMP_ROOT/bootstrap" ./get.sh --dry-run --profile=minimal \
| tee "$TMP_ROOT/get-dry-run.txt"
grep -q 'Would clone' "$TMP_ROOT/get-dry-run.txt"
test ! -e "$TMP_ROOT/bootstrap"
mkdir -p "$TMP_ROOT/no-git-bin"
printf '%s\n' \
'#!/usr/bin/env bash' \
"echo \"brew was called\" >> \"\$BREW_LOG\"" \
'exit 42' > "$TMP_ROOT/no-git-bin/brew"
chmod +x "$TMP_ROOT/no-git-bin/brew"
BREW_LOG="$TMP_ROOT/no-git-brew.log" \
PATH="$TMP_ROOT/no-git-bin" \
CHOPSTICKS_DEST="$TMP_ROOT/no-git-bootstrap" \
/bin/bash ./get.sh --dry-run --profile=full \
| tee "$TMP_ROOT/get-no-git-dry-run.txt"
grep -q 'Would require: git' "$TMP_ROOT/get-no-git-dry-run.txt"
grep -q 'Would clone' "$TMP_ROOT/get-no-git-dry-run.txt"
test ! -e "$TMP_ROOT/no-git-brew.log"
test ! -e "$TMP_ROOT/no-git-bootstrap"
mkdir -p "$TMP_ROOT/not-chopsticks"
git -c init.defaultBranch=main init "$TMP_ROOT/not-chopsticks" >/dev/null
git -C "$TMP_ROOT/not-chopsticks" remote add origin https://github.com/example/not-chopsticks.git
if CHOPSTICKS_DEST="$TMP_ROOT/not-chopsticks" ./get.sh --dry-run; then
echo "Expected get.sh to reject non-chopsticks repo" >&2
exit 1
fi
mkdir -p "$TMP_ROOT/chopsticks-existing"
git -c init.defaultBranch=main init "$TMP_ROOT/chopsticks-existing" >/dev/null
git -C "$TMP_ROOT/chopsticks-existing" remote add origin https://github.com/m1ngsama/chopsticks.git
touch "$TMP_ROOT/chopsticks-existing/install.sh" "$TMP_ROOT/chopsticks-existing/.vimrc"
CHOPSTICKS_DEST="$TMP_ROOT/chopsticks-existing" ./get.sh --dry-run --yes \
| tee "$TMP_ROOT/get-existing.txt"
grep -q 'Would update existing chopsticks repo' "$TMP_ROOT/get-existing.txt"
}
check_plugin_dirs() {
step "Plugin directories"
for plugin in \
fzf fzf.vim vim-fugitive vim-gitgutter ale vim-lsp vim-lsp-settings \
asyncomplete.vim asyncomplete-lsp.vim vim-markdown
do
test -d "$HOME/.vim/plugged/$plugin" || {
echo "Missing plugin directory: $plugin" >&2
exit 1
}
done
}
check_vim() {
step "Vim smoke tests"
need vim
check_plugin_dirs
XDG_CONFIG_HOME="$EMPTY_XDG" vim -u .vimrc -i NONE -es -N -c 'qa!' 2>&1
XDG_CONFIG_HOME="$EMPTY_XDG" vim -u .vimrc -i NONE -es -N \
-c "redir! > $TMP_ROOT/plugs.txt" \
-c 'silent echo len(g:plugs)' \
-c 'redir END' \
-c 'qa!' 2>/dev/null
PLUGS="$(tr -d '[:space:]' < "$TMP_ROOT/plugs.txt")"
echo "Plugins registered: $PLUGS"
if [ "$PLUGS" -lt 20 ]; then
echo "Expected 20+ plugins, got $PLUGS" >&2
exit 1
fi
mkdir -p "$TMP_ROOT/chopsticks path/modules"
cp .vimrc "$TMP_ROOT/chopsticks path/.vimrc"
cp modules/*.vim "$TMP_ROOT/chopsticks path/modules/"
XDG_CONFIG_HOME="$EMPTY_XDG" vim -u "$TMP_ROOT/chopsticks path/.vimrc" \
-i NONE -es -N -c 'qa!' 2>&1
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' \
-c 'qa!' 2>&1
mkdir -p "$TMP_ROOT/local"
printf "%s\n" "let g:chopsticks_profile = 'minimal'" > "$TMP_ROOT/local/config.vim"
vim -u NONE -i NONE -es -N \
-c "let g:chopsticks_local_config = '$TMP_ROOT/local/config.vim'" \
-c 'source .vimrc' \
-c 'if g:chopsticks_profile !=# "minimal" || has_key(g:plugs, "ale") || has_key(g:plugs, "vim-lsp") || has_key(g:plugs, "auto-pairs") | cquit | endif' \
-c 'qa!' 2>&1
mkdir -p "$TMP_ROOT/xdg"
printf "%s\n" "let g:chopsticks_profile = 'minimal'" > "$TMP_ROOT/xdg/chopsticks.vim"
XDG_CONFIG_HOME="$TMP_ROOT/xdg" vim -u NONE -i NONE -es -N \
-c 'source .vimrc' \
-c 'if g:chopsticks_profile !=# "minimal" || has_key(g:plugs, "ale") || has_key(g:plugs, "vim-lsp") || has_key(g:plugs, "auto-pairs") | cquit | endif' \
-c 'qa!' 2>&1
XDG_CONFIG_HOME="$EMPTY_XDG" vim -u .vimrc -i NONE -es -N \
-c 'ChopsticksStatus' \
-c "redir! > $TMP_ROOT/status-default.txt" \
-c 'silent %print' \
-c 'redir END' \
-c 'qa!' 2>&1
if grep -Fq 'vim-lsp not loaded' "$TMP_ROOT/status-default.txt"; then
cat "$TMP_ROOT/status-default.txt"
exit 1
fi
grep -Fq 'OK vim-lsp stack (installed)' "$TMP_ROOT/status-default.txt"
grep -Fq 'python (:LspInstallServer in a python file)' "$TMP_ROOT/status-default.txt"
grep -Fq 'LSP actions are buffer-local and start after a server attaches.' "$TMP_ROOT/status-default.txt"
grep -Fq 'Open that filetype and run :LspInstallServer once.' "$TMP_ROOT/status-default.txt"
XDG_CONFIG_HOME="$EMPTY_XDG" vim -u .vimrc -i NONE -es -N \
-c 'let last_change_map = nr2char(96) . "[v" . nr2char(96) . "]"' \
-c 'if maparg("0", "n") !=# "" || maparg("0", "v") !=# "" || maparg("Y", "n") !=# "" || maparg("Q", "n") !=# "" || maparg("<Space>", "n") !=# "" || maparg("//", "v") !=# "" || maparg("gV", "n") !=# "" || maparg("jk", "i") !=# "" || maparg("<C-s>", "n") !=# "" || maparg("<C-s>", "i") !=# "" || maparg("<C-h>", "n") !=# "" || maparg("<C-j>", "n") !=# "" || maparg("<C-k>", "n") !=# "" || maparg("<C-l>", "n") !=# "" || maparg("<C-p>", "n") !=# "" || maparg("<C-p>", "c") !=# "" || maparg("<C-n>", "c") !=# "" || maparg("w!!", "c") !=# "" | cquit | endif' \
-c 'if has_key(g:plugs, "auto-pairs") || maparg("<Tab>", "i") =~# "pumvisible" || maparg("<S-Tab>", "i") =~# "pumvisible" || maparg("<CR>", "i") =~# "asyncomplete#close_popup" || maparg("<CR>", "i") =~# "AutoPairs" | cquit | endif' \
-c 'if maparg("<Esc><Esc>", "t") !=# "" || maparg("<C-h>", "t") !=# "" || maparg("<C-j>", "t") !=# "" || maparg("<C-k>", "t") !=# "" || maparg("<C-l>", "t") !=# "" | cquit | endif' \
-c 'if maparg(",/", "v") !~# "escape" || maparg(",v", "n") !=# last_change_map || maparg(",ff", "n") !~# "SmartFiles" | cquit | endif' \
-c 'qa!' 2>&1
XDG_CONFIG_HOME="$EMPTY_XDG" vim -u NONE -i NONE -es -N \
-c 'let g:chopsticks_enable_jk_escape = 1' \
-c 'source .vimrc' \
-c 'if maparg("jk", "i") !~# "<Esc>" | cquit | endif' \
-c 'qa!' 2>&1
XDG_CONFIG_HOME="$EMPTY_XDG" vim -u NONE -i NONE -es -N \
-c 'let g:chopsticks_enable_ctrl_s_save = 1' \
-c 'let g:chopsticks_enable_sudo_save_bang = 1' \
-c 'let g:chopsticks_enable_completion_keymaps = 1' \
-c 'source .vimrc' \
-c 'if maparg("<C-s>", "n") !~# ":w" || maparg("<C-s>", "i") !~# ":w" || maparg("w!!", "c") !~# "sudo tee" | cquit | endif' \
-c 'if maparg("<Tab>", "i") !~# "pumvisible" || maparg("<S-Tab>", "i") !~# "pumvisible" || maparg("<CR>", "i") !~# "asyncomplete#close_popup" | cquit | endif' \
-c 'qa!' 2>&1
XDG_CONFIG_HOME="$EMPTY_XDG" vim -u NONE -i NONE -es -N \
-c 'let g:chopsticks_enable_auto_pairs = 1' \
-c 'source .vimrc' \
-c 'if !has_key(g:plugs, "auto-pairs") | cquit | endif' \
-c 'qa!' 2>&1
XDG_CONFIG_HOME="$EMPTY_XDG" vim -u NONE -i NONE -es -N \
-c 'let g:chopsticks_enable_terminal_keymaps = 1' \
-c 'source .vimrc' \
-c 'if has("terminal") && (maparg("<Esc><Esc>", "t") !~# "<C-\\\\><C-N>" || maparg("<C-h>", "t") !~# "<C-W>h" || maparg("<C-j>", "t") !~# "<C-W>j" || maparg("<C-k>", "t") !~# "<C-W>k" || maparg("<C-l>", "t") !~# "<C-W>l") | cquit | endif' \
-c 'qa!' 2>&1
XDG_CONFIG_HOME="$EMPTY_XDG" vim -u .vimrc -i NONE -es -N \
-c 'if &exrc || &secure | cquit | endif' \
-c 'qa!' 2>&1
XDG_CONFIG_HOME="$EMPTY_XDG" vim -u NONE -i NONE -es -N \
-c 'let g:chopsticks_enable_exrc = 1' \
-c 'source .vimrc' \
-c 'if !&exrc || !&secure | cquit | endif' \
-c 'qa!' 2>&1
XDG_CONFIG_HOME="$EMPTY_XDG" vim -u .vimrc -i NONE -es -N \
-c 'if maparg(",F", "n") !=# "" | cquit | endif' \
-c 'if maparg(",F", "v") !~# "=" | cquit | endif' \
-c 'qa!' 2>&1
XDG_CONFIG_HOME="$EMPTY_XDG" vim -u NONE -i NONE -es -N \
-c 'let g:chopsticks_enable_reindent_file = 1' \
-c 'source .vimrc' \
-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' \
-c 'ChopsticksStatus' \
-c "redir! > $TMP_ROOT/status-lsp-not-loaded.txt" \
-c 'silent %print' \
-c 'redir END' \
-c 'qa!' 2>&1
grep -Fq 'OK vim-lsp stack (installed; not loaded yet)' "$TMP_ROOT/status-lsp-not-loaded.txt"
XDG_CONFIG_HOME="$EMPTY_XDG" vim -u NONE -i NONE -es -N \
-c 'let g:chopsticks_profile = "minimal"' \
-c 'source .vimrc' \
-c 'ChopsticksStatus' \
-c "redir! > $TMP_ROOT/status-minimal.txt" \
-c 'silent %print' \
-c 'redir END' \
-c 'qa!' 2>&1
grep -Fq 'off vim-lsp stack (LSP disabled by profile)' "$TMP_ROOT/status-minimal.txt"
grep -Fq 'off python (LSP disabled by profile)' "$TMP_ROOT/status-minimal.txt"
if grep -Fq 'LSP actions are buffer-local' "$TMP_ROOT/status-minimal.txt"; then
cat "$TMP_ROOT/status-minimal.txt"
exit 1
fi
mkdir -p "$TMP_ROOT/missing-home/.vim/autoload"
cp "$HOME/.vim/autoload/plug.vim" "$TMP_ROOT/missing-home/.vim/autoload/plug.vim"
HOME="$TMP_ROOT/missing-home" XDG_CONFIG_HOME="$EMPTY_XDG" \
vim -u .vimrc -i NONE -es -N \
-c 'ChopsticksStatus' \
-c "redir! > $TMP_ROOT/status-missing-plugin.txt" \
-c 'silent %print' \
-c 'redir END' \
-c 'qa!' 2>&1
grep -Fq 'vim-lsp not installed; run :PlugInstall' "$TMP_ROOT/status-missing-plugin.txt"
XDG_CONFIG_HOME="$EMPTY_XDG" vim -u .vimrc -i NONE -es -N \
-c 'doautocmd User lsp_buffer_enabled' \
-c 'if maparg("gd", "n") !=# "" || maparg("K", "n") !=# "" || maparg("gi", "n") !=# "" || maparg("gr", "n") !=# "" | cquit | endif' \
-c 'if maparg(",dd", "n") !~# "lsp-definition" | cquit | endif' \
-c 'if maparg(",dt", "n") !~# "lsp-type-definition" | cquit | endif' \
-c 'if maparg(",di", "n") !~# "lsp-implementation" | cquit | endif' \
-c 'if maparg(",dr", "n") !~# "lsp-references" | cquit | endif' \
-c 'if maparg(",dk", "n") !~# "lsp-hover" | cquit | endif' \
-c 'if maparg(",dp", "n") !~# "lsp-previous-diagnostic" | cquit | endif' \
-c 'if maparg(",dn", "n") !~# "lsp-next-diagnostic" | cquit | endif' \
-c 'qa!' 2>&1
XDG_CONFIG_HOME="$EMPTY_XDG" vim -u .vimrc -i NONE -es -N \
-c 'normal ,?' \
-c "redir! > $TMP_ROOT/cheat-default.txt" \
-c 'silent %print' \
-c 'redir END' \
-c 'qa!' 2>&1
grep -Fq ':ChopsticksStatus check LSP setup' "$TMP_ROOT/cheat-default.txt"
grep -Fq ',ff files' "$TMP_ROOT/cheat-default.txt"
grep -Fq ',dd definition' "$TMP_ROOT/cheat-default.txt"
grep -Fq ',dk hover docs' "$TMP_ROOT/cheat-default.txt"
grep -Fq ',dp ,dn LSP diagnostics' "$TMP_ROOT/cheat-default.txt"
grep -Fq '<C-w>hjkl navigate splits' "$TMP_ROOT/cheat-default.txt"
if grep -Eq 'Ctrl\\+p find file|Ctrl\\+hjkl navigate splits|Ctrl\\+s save|jk exit insert|gd definition|K hover docs|\\[g \\]g LSP diagnostics' "$TMP_ROOT/cheat-default.txt"; then
cat "$TMP_ROOT/cheat-default.txt"
exit 1
fi
XDG_CONFIG_HOME="$EMPTY_XDG" vim -u NONE -i NONE -es -N \
-c 'let g:chopsticks_profile = "minimal"' \
-c 'source .vimrc' \
-c 'normal ,?' \
-c "redir! > $TMP_ROOT/cheat.txt" \
-c 'silent %print' \
-c 'redir END' \
-c 'qa!' 2>&1
if grep -Eq 'definition|LspInstallServer|ALE errors|undo tree|markdown preview' "$TMP_ROOT/cheat.txt"; then
cat "$TMP_ROOT/cheat.txt"
exit 1
fi
grep -q ',cr run file' "$TMP_ROOT/cheat.txt"
XDG_CONFIG_HOME="$EMPTY_XDG" vim -u .vimrc -i NONE -es -N README.md \
-c 'set filetype=markdown' \
-c 'if &l:spell || &l:conceallevel != 0 || &l:signcolumn !=# "no" || exists("g:lsp_settings_filetype_markdown") | cquit | endif' \
-c 'qa!' 2>&1
XDG_CONFIG_HOME="$EMPTY_XDG" vim -u .vimrc -i NONE -es -N \
-c 'if maparg("s", "n") !=# "" | cquit | endif' \
-c 'if maparg(",w", "n") =~# "!" | cquit | endif' \
-c 'if !&swapfile || !&writebackup || &directory !~# "\.vim/.swap" | cquit | endif' \
-c 'qa!' 2>&1
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' \
-c 'qa!' 2>&1
truncate -s 11000000 "$TMP_ROOT/large.py"
XDG_CONFIG_HOME="$EMPTY_XDG" vim -u .vimrc -i NONE -es -N "$TMP_ROOT/large.py" \
-c 'set filetype=python' \
-c 'if &l:syntax !=# "" || &l:undolevels != -1 || &l:swapfile || get(b:, "ale_enabled", 1) != 0 | cquit | endif' \
-c 'qa!' 2>&1
mkdir -p "$TMP_ROOT/fake-bin" "$TMP_ROOT/c runner"
cat > "$TMP_ROOT/fake-bin/gcc" <<'GCCEOF'
#!/usr/bin/env bash
set -eu
printf '%s\n' "$@" > "$GCC_ARGS"
out=""
while [ "$#" -gt 0 ]; do
if [ "$1" = "-o" ]; then
shift
out="$1"
fi
shift || true
done
test -n "$out"
printf '%s\n' '#!/usr/bin/env bash' 'exit 0' > "$out"
chmod +x "$out"
GCCEOF
chmod +x "$TMP_ROOT/fake-bin/gcc"
c_file="$TMP_ROOT/c runner/main.c"
c_file_real="$(cd "$TMP_ROOT/c runner" && pwd -P)/main.c"
printf '%s\n' 'int main(void) { return 0; }' > "$c_file"
GCC_ARGS="$TMP_ROOT/gcc-args.txt" \
PATH="$TMP_ROOT/fake-bin:$PATH" \
XDG_CONFIG_HOME="$EMPTY_XDG" \
vim -u .vimrc -i NONE -es -N "$c_file" \
-c 'set filetype=c' \
-c 'normal ,cr' \
-c 'qa!' 2>&1
c_out="$(sed -n '2p' "$TMP_ROOT/gcc-args.txt")"
test -n "$c_out"
test "$c_out" != "/tmp/a.out"
test ! -e "$c_out"
grep -Fxq "$c_file_real" "$TMP_ROOT/gcc-args.txt"
XDG_CONFIG_HOME="$EMPTY_XDG" vim -u .vimrc -i NONE --startuptime "$TMP_ROOT/startup.log" \
-es -N -c 'qa!' 2>/dev/null
tail -1 "$TMP_ROOT/startup.log"
STARTUP_MS="$(awk 'END { print $1 }' "$TMP_ROOT/startup.log")"
awk -v ms="$STARTUP_MS" -v limit="$STARTUP_LIMIT_MS" \
'BEGIN { if (ms > limit) exit 1 }'
}
run_group() {
case "$1" in
quick)
check_shell
check_docs
check_installer_modes
check_bootstrap
quick | shell | docs | installer | bootstrap)
bash "$SCRIPT_DIR/test-quick.sh" "$1"
;;
vim)
bash "$SCRIPT_DIR/test-vim.sh"
;;
shell) check_shell ;;
docs) check_docs ;;
installer) check_installer_modes ;;
bootstrap) check_bootstrap ;;
vim) check_vim ;;
all)
run_group quick
check_vim
bash "$SCRIPT_DIR/test-quick.sh" quick
bash "$SCRIPT_DIR/test-vim.sh"
;;
list | --list) list_groups ;;
-h | --help) usage ;;