mirror of
https://github.com/ksyasuda/dotfiles.git
synced 2026-09-10 17:16:18 -07:00
655 lines
28 KiB
HTML
655 lines
28 KiB
HTML
<!doctype html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<meta name="color-scheme" content="dark">
|
||
<title>Neovim Config Field Manual</title>
|
||
<script>document.documentElement.classList.add("js");</script>
|
||
<style>
|
||
:root {
|
||
--base: #24273a;
|
||
--mantle: #1e2030;
|
||
--crust: #181926;
|
||
--surface: #363a4f;
|
||
--surface-high: #494d64;
|
||
--text: #cad3f5;
|
||
--subtext: #a5adcb;
|
||
--muted: #6e738d;
|
||
--green: #a6da95;
|
||
--teal: #8bd5ca;
|
||
--yellow: #eed49f;
|
||
--peach: #f5a97f;
|
||
--red: #ed8796;
|
||
--blue: #8aadf4;
|
||
--mauve: #c6a0f6;
|
||
--line: color-mix(in srgb, var(--surface-high) 72%, transparent);
|
||
--mono: "JetBrains Mono", "Cascadia Code", "SFMono-Regular", monospace;
|
||
--serif: "Iowan Old Style", "Baskerville", "Palatino Linotype", serif;
|
||
}
|
||
|
||
* { box-sizing: border-box; }
|
||
html { scroll-behavior: smooth; }
|
||
body {
|
||
margin: 0;
|
||
color: var(--text);
|
||
background:
|
||
radial-gradient(circle at 78% 12%, rgb(139 213 202 / 9%), transparent 26rem),
|
||
linear-gradient(90deg, rgb(255 255 255 / 2%) 1px, transparent 1px),
|
||
linear-gradient(rgb(255 255 255 / 2%) 1px, transparent 1px),
|
||
var(--crust);
|
||
background-size: auto, 44px 44px, 44px 44px, auto;
|
||
font-family: var(--serif);
|
||
font-size: 17px;
|
||
line-height: 1.65;
|
||
}
|
||
|
||
a { color: inherit; }
|
||
button { font: inherit; }
|
||
::selection { color: var(--crust); background: var(--green); }
|
||
:focus-visible {
|
||
outline: 2px solid var(--yellow);
|
||
outline-offset: 4px;
|
||
}
|
||
|
||
.reading-progress {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
z-index: 20;
|
||
width: 0;
|
||
height: 3px;
|
||
background: linear-gradient(90deg, var(--green), var(--teal));
|
||
box-shadow: 0 0 18px var(--teal);
|
||
}
|
||
|
||
.layout {
|
||
display: grid;
|
||
grid-template-columns: 18rem minmax(0, 1fr);
|
||
min-height: 100vh;
|
||
}
|
||
|
||
.rail {
|
||
position: sticky;
|
||
top: 0;
|
||
height: 100vh;
|
||
padding: 2rem 1.5rem;
|
||
border-right: 1px solid var(--line);
|
||
background: rgb(24 25 38 / 84%);
|
||
backdrop-filter: blur(14px);
|
||
}
|
||
|
||
.brand {
|
||
display: grid;
|
||
grid-template-columns: 2.5rem 1fr;
|
||
gap: 0.8rem;
|
||
align-items: center;
|
||
margin-bottom: 3rem;
|
||
}
|
||
|
||
.brand-mark {
|
||
display: grid;
|
||
place-items: center;
|
||
width: 2.5rem;
|
||
aspect-ratio: 1;
|
||
color: var(--crust);
|
||
background: var(--green);
|
||
font: 800 1.35rem/1 var(--mono);
|
||
clip-path: polygon(16% 0, 100% 0, 84% 100%, 0 100%);
|
||
}
|
||
|
||
.brand strong,
|
||
.brand small { display: block; font-family: var(--mono); }
|
||
.brand strong { color: var(--green); font-size: 0.82rem; letter-spacing: 0.12em; text-transform: uppercase; }
|
||
.brand small { color: var(--muted); font-size: 0.68rem; }
|
||
|
||
.nav-label,
|
||
.eyebrow,
|
||
.kicker {
|
||
font: 700 0.68rem/1.2 var(--mono);
|
||
letter-spacing: 0.16em;
|
||
text-transform: uppercase;
|
||
}
|
||
|
||
.nav-label { color: var(--muted); }
|
||
.rail nav { display: grid; gap: 0.22rem; margin-top: 0.8rem; }
|
||
.rail nav a {
|
||
position: relative;
|
||
padding: 0.55rem 0.75rem 0.55rem 1.2rem;
|
||
color: var(--subtext);
|
||
font: 500 0.76rem/1.3 var(--mono);
|
||
text-decoration: none;
|
||
transition: color 160ms ease, transform 160ms ease;
|
||
}
|
||
.rail nav a::before {
|
||
content: "";
|
||
position: absolute;
|
||
left: 0;
|
||
top: 50%;
|
||
width: 0.45rem;
|
||
height: 1px;
|
||
background: var(--surface-high);
|
||
transition: width 160ms ease, background 160ms ease;
|
||
}
|
||
.rail nav a:hover,
|
||
.rail nav a.active { color: var(--green); transform: translateX(0.18rem); }
|
||
.rail nav a.active::before { width: 0.8rem; background: var(--green); }
|
||
|
||
.rail-foot {
|
||
position: absolute;
|
||
left: 1.5rem;
|
||
right: 1.5rem;
|
||
bottom: 1.7rem;
|
||
padding-top: 1rem;
|
||
border-top: 1px solid var(--line);
|
||
color: var(--muted);
|
||
font: 0.68rem/1.55 var(--mono);
|
||
}
|
||
|
||
main { min-width: 0; overflow: hidden; }
|
||
.hero,
|
||
.section { padding-inline: clamp(2rem, 7vw, 7rem); }
|
||
|
||
.hero {
|
||
position: relative;
|
||
display: grid;
|
||
align-content: center;
|
||
min-height: 92vh;
|
||
padding-block: 6rem;
|
||
border-bottom: 1px solid var(--line);
|
||
}
|
||
|
||
.hero::after {
|
||
content: "NVIM";
|
||
position: absolute;
|
||
right: -0.08em;
|
||
bottom: -0.24em;
|
||
z-index: -1;
|
||
color: rgb(166 218 149 / 4%);
|
||
font: 900 clamp(8rem, 22vw, 21rem)/1 var(--mono);
|
||
letter-spacing: -0.12em;
|
||
pointer-events: none;
|
||
}
|
||
|
||
.eyebrow { color: var(--teal); }
|
||
h1 {
|
||
max-width: 12ch;
|
||
margin: 1.2rem 0 1.4rem;
|
||
font: 800 clamp(3.2rem, 8vw, 7.8rem)/0.91 var(--mono);
|
||
letter-spacing: -0.07em;
|
||
}
|
||
h1 .outline { color: transparent; -webkit-text-stroke: 1px var(--green); }
|
||
.lede { max-width: 43rem; margin: 0; color: var(--subtext); font-size: clamp(1.1rem, 2vw, 1.4rem); }
|
||
|
||
.hero-meta {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 0.6rem;
|
||
margin-top: 2.5rem;
|
||
}
|
||
.chip {
|
||
padding: 0.42rem 0.7rem;
|
||
border: 1px solid var(--line);
|
||
color: var(--subtext);
|
||
background: rgb(36 39 58 / 70%);
|
||
font: 0.68rem/1 var(--mono);
|
||
letter-spacing: 0.04em;
|
||
}
|
||
.chip.good { color: var(--green); border-color: rgb(166 218 149 / 40%); }
|
||
|
||
.section {
|
||
padding-block: clamp(5rem, 10vw, 9rem);
|
||
border-bottom: 1px solid var(--line);
|
||
}
|
||
|
||
.section-head {
|
||
display: grid;
|
||
grid-template-columns: minmax(0, 0.45fr) minmax(20rem, 1fr);
|
||
gap: clamp(2rem, 6vw, 7rem);
|
||
margin-bottom: 4rem;
|
||
}
|
||
.section-number { color: var(--muted); font: 0.72rem var(--mono); }
|
||
h2 { margin: 0.55rem 0 0; font: 750 clamp(2rem, 4.5vw, 4.6rem)/1 var(--mono); letter-spacing: -0.055em; }
|
||
.section-head p { align-self: end; max-width: 43rem; margin: 0; color: var(--subtext); font-size: 1.08rem; }
|
||
|
||
.boot-flow {
|
||
display: grid;
|
||
grid-template-columns: repeat(7, minmax(8rem, 1fr));
|
||
overflow-x: auto;
|
||
padding: 1.5rem 0 0.6rem;
|
||
scrollbar-color: var(--surface-high) transparent;
|
||
}
|
||
.boot-step {
|
||
position: relative;
|
||
min-height: 13rem;
|
||
padding: 1.1rem 1rem;
|
||
border-top: 1px solid var(--surface-high);
|
||
border-bottom: 1px solid var(--surface-high);
|
||
border-left: 1px solid var(--surface-high);
|
||
background: linear-gradient(180deg, rgb(54 58 79 / 52%), rgb(30 32 48 / 62%));
|
||
}
|
||
.boot-step:last-child { border-right: 1px solid var(--surface-high); }
|
||
.boot-step::after {
|
||
content: "›";
|
||
position: absolute;
|
||
right: -0.5rem;
|
||
top: 50%;
|
||
z-index: 2;
|
||
color: var(--green);
|
||
font: 1.4rem var(--mono);
|
||
transform: translateY(-50%);
|
||
}
|
||
.boot-step:last-child::after { display: none; }
|
||
.boot-index { color: var(--green); font: 0.72rem var(--mono); }
|
||
.boot-step h3 { margin: 2.4rem 0 0.8rem; font: 700 0.82rem/1.3 var(--mono); }
|
||
.boot-step p { margin: 0; color: var(--muted); font: 0.72rem/1.55 var(--mono); }
|
||
|
||
.ownership {
|
||
display: grid;
|
||
grid-template-columns: 0.8fr 1.2fr;
|
||
border: 1px solid var(--line);
|
||
background: rgb(30 32 48 / 58%);
|
||
}
|
||
.owner-list { border-right: 1px solid var(--line); }
|
||
.owner {
|
||
display: grid;
|
||
grid-template-columns: 2.2rem 1fr;
|
||
gap: 0.8rem;
|
||
padding: 1.1rem 1.2rem;
|
||
border-bottom: 1px solid var(--line);
|
||
cursor: pointer;
|
||
transition: background 150ms ease;
|
||
}
|
||
.owner:last-child { border-bottom: 0; }
|
||
.owner:hover,
|
||
.owner.active { background: rgb(139 213 202 / 8%); }
|
||
.owner-key { color: var(--teal); font: 700 0.72rem var(--mono); }
|
||
.owner strong { display: block; font: 700 0.8rem var(--mono); }
|
||
.owner span { color: var(--muted); font: 0.7rem var(--mono); }
|
||
.owner-detail { display: grid; align-content: center; min-height: 26rem; padding: clamp(2rem, 5vw, 5rem); }
|
||
.owner-detail > div { display: none; }
|
||
.owner-detail > div.active { display: block; animation: detail-in 240ms ease both; }
|
||
.owner-detail .kicker { color: var(--yellow); }
|
||
.owner-detail h3 { margin: 0.8rem 0 1rem; font: 700 clamp(1.6rem, 3vw, 3rem)/1.05 var(--mono); }
|
||
.owner-detail p { max-width: 38rem; color: var(--subtext); }
|
||
|
||
.split {
|
||
display: grid;
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
gap: 1px;
|
||
background: var(--line);
|
||
border: 1px solid var(--line);
|
||
}
|
||
.panel { padding: clamp(1.5rem, 4vw, 3.5rem); background: var(--mantle); }
|
||
.panel .kicker { color: var(--peach); }
|
||
.panel h3 { margin: 1rem 0; font: 700 1.35rem var(--mono); }
|
||
.panel p { color: var(--subtext); }
|
||
|
||
code,
|
||
pre { font-family: var(--mono); }
|
||
code { color: var(--teal); font-size: 0.86em; }
|
||
pre {
|
||
position: relative;
|
||
overflow: auto;
|
||
margin: 1.5rem 0 0;
|
||
padding: 1.3rem;
|
||
border: 1px solid var(--surface);
|
||
background: var(--crust);
|
||
color: var(--text);
|
||
font-size: 0.76rem;
|
||
line-height: 1.7;
|
||
tab-size: 2;
|
||
}
|
||
.lua-key { color: var(--mauve); }
|
||
.lua-str { color: var(--green); }
|
||
.lua-note { color: var(--muted); }
|
||
|
||
.mapping-rule {
|
||
display: grid;
|
||
grid-template-columns: 1fr auto 1fr;
|
||
gap: 1.2rem;
|
||
align-items: stretch;
|
||
}
|
||
.rule-box { padding: 2rem; border: 1px solid var(--line); background: rgb(36 39 58 / 76%); }
|
||
.rule-box h3 { margin: 0 0 1rem; color: var(--green); font: 700 1rem var(--mono); }
|
||
.rule-box p { margin-bottom: 0; color: var(--subtext); }
|
||
.rule-arrow { display: grid; place-items: center; color: var(--muted); font: 1.6rem var(--mono); }
|
||
|
||
.removed {
|
||
display: grid;
|
||
grid-template-columns: repeat(3, 1fr);
|
||
gap: 1px;
|
||
background: var(--line);
|
||
border: 1px solid var(--line);
|
||
}
|
||
.removed article { min-height: 13rem; padding: 1.5rem; background: var(--mantle); }
|
||
.removed del { color: var(--red); font: 700 0.75rem var(--mono); text-decoration-thickness: 2px; }
|
||
.removed strong { display: block; margin-top: 2rem; color: var(--green); font: 0.84rem var(--mono); }
|
||
.removed p { color: var(--muted); font-size: 0.9rem; }
|
||
|
||
.checks { display: grid; gap: 0.8rem; max-width: 64rem; }
|
||
.check {
|
||
display: grid;
|
||
grid-template-columns: 3rem 1fr auto;
|
||
gap: 1rem;
|
||
align-items: center;
|
||
padding: 1rem;
|
||
border: 1px solid var(--line);
|
||
background: var(--mantle);
|
||
}
|
||
.check-state { color: var(--green); font: 700 0.75rem var(--mono); }
|
||
.check code { overflow-wrap: anywhere; }
|
||
.copy {
|
||
padding: 0.5rem 0.7rem;
|
||
border: 1px solid var(--surface-high);
|
||
color: var(--subtext);
|
||
background: transparent;
|
||
font: 0.65rem var(--mono);
|
||
cursor: pointer;
|
||
}
|
||
.copy:hover { color: var(--crust); border-color: var(--green); background: var(--green); }
|
||
|
||
.callout {
|
||
margin-top: 3rem;
|
||
padding: 1.4rem 1.5rem;
|
||
border-left: 3px solid var(--yellow);
|
||
background: rgb(238 212 159 / 7%);
|
||
color: var(--subtext);
|
||
}
|
||
.callout strong { color: var(--yellow); font-family: var(--mono); }
|
||
|
||
footer {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
gap: 2rem;
|
||
padding: 3rem clamp(2rem, 7vw, 7rem);
|
||
color: var(--muted);
|
||
font: 0.68rem var(--mono);
|
||
}
|
||
|
||
.js .reveal { opacity: 0; transform: translateY(1rem); }
|
||
.reveal.visible { animation: reveal 600ms cubic-bezier(0.2, 0.8, 0.2, 1) both; }
|
||
@keyframes reveal { to { opacity: 1; transform: none; } }
|
||
@keyframes detail-in { from { opacity: 0; transform: translateY(0.5rem); } to { opacity: 1; transform: none; } }
|
||
|
||
@media (max-width: 900px) {
|
||
.layout { display: block; }
|
||
.rail { position: relative; height: auto; padding: 1rem; border-right: 0; border-bottom: 1px solid var(--line); }
|
||
.brand { margin: 0; }
|
||
.rail .nav-label,
|
||
.rail nav,
|
||
.rail-foot { display: none; }
|
||
.hero { min-height: 76vh; }
|
||
.section-head,
|
||
.ownership,
|
||
.split { grid-template-columns: 1fr; }
|
||
.owner-list { border-right: 0; border-bottom: 1px solid var(--line); }
|
||
.mapping-rule { grid-template-columns: 1fr; }
|
||
.rule-arrow { transform: rotate(90deg); }
|
||
.removed { grid-template-columns: 1fr 1fr; }
|
||
}
|
||
|
||
@media (max-width: 560px) {
|
||
.hero,
|
||
.section { padding-inline: 1.2rem; }
|
||
h1 { font-size: 3.3rem; }
|
||
.section-head { grid-template-columns: 1fr; }
|
||
.removed { grid-template-columns: 1fr; }
|
||
.check { grid-template-columns: 2.5rem 1fr; }
|
||
.copy { grid-column: 2; justify-self: start; }
|
||
footer { flex-direction: column; padding-inline: 1.2rem; }
|
||
}
|
||
|
||
@media (prefers-reduced-motion: reduce) {
|
||
html { scroll-behavior: auto; }
|
||
*, *::before, *::after { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; }
|
||
.reveal { opacity: 1; transform: none; }
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="reading-progress" aria-hidden="true"></div>
|
||
<div class="layout">
|
||
<aside class="rail">
|
||
<div class="brand">
|
||
<div class="brand-mark">N</div>
|
||
<div><strong>Field manual</strong><small>~/.config/nvim</small></div>
|
||
</div>
|
||
<div class="nav-label">Index</div>
|
||
<nav aria-label="Page sections">
|
||
<a href="#boot">01 / Boot sequence</a>
|
||
<a href="#ownership">02 / Tool ownership</a>
|
||
<a href="#keymaps">03 / Keymap model</a>
|
||
<a href="#plugins">04 / Plugin loading</a>
|
||
<a href="#cleanup">05 / What left</a>
|
||
<a href="#checks">06 / Maintenance loop</a>
|
||
</nav>
|
||
<div class="rail-foot">Macchiato palette<br>Lua modules + Lazy specs<br>Generated 2026.08</div>
|
||
</aside>
|
||
|
||
<main>
|
||
<header class="hero">
|
||
<div class="eyebrow">Neovim configuration / annotated system map</div>
|
||
<h1>Small core.<br><span class="outline">Deep tools.</span></h1>
|
||
<p class="lede">A practical map of how this Lua configuration boots, where behavior belongs, and which plugin owns each editing concern.</p>
|
||
<div class="hero-meta">
|
||
<span class="chip good">58 declared plugins</span>
|
||
<span class="chip">Lazy-loaded mappings</span>
|
||
<span class="chip">No LSP format fallback</span>
|
||
<span class="chip">0 Luacheck warnings</span>
|
||
</div>
|
||
</header>
|
||
|
||
<section class="section" id="boot">
|
||
<div class="section-head reveal">
|
||
<div><span class="section-number">01</span><h2>Boot sequence</h2></div>
|
||
<p><code>init.lua</code> is intentionally boring. It establishes Lazy first, then layers presentation, mappings, editor events, highlights, and one specialized LSP helper.</p>
|
||
</div>
|
||
<div class="boot-flow reveal" aria-label="Neovim startup sequence">
|
||
<article class="boot-step"><span class="boot-index">01</span><h3>core.lazy</h3><p>Bootstraps Lazy and loads core options before plugins.</p></article>
|
||
<article class="boot-step"><span class="boot-index">02</span><h3>plugins/*</h3><p>Lazy discovers specs and installs handlers for events, commands, and keys.</p></article>
|
||
<article class="boot-step"><span class="boot-index">03</span><h3>colorscheme</h3><p>Catppuccin Macchiato supplies the visual baseline.</p></article>
|
||
<article class="boot-step"><span class="boot-index">04</span><h3>core.keymaps</h3><p>Core editing, LSP, commands, and group labels register.</p></article>
|
||
<article class="boot-step"><span class="boot-index">05</span><h3>autocmds</h3><p>Editor-level reactions attach once through named augroups.</p></article>
|
||
<article class="boot-step"><span class="boot-index">06</span><h3>highlights</h3><p>Small local highlight overrides apply after the theme.</p></article>
|
||
<article class="boot-step"><span class="boot-index">07</span><h3>Hyprland LSP</h3><p>The file-pattern-specific helper starts only where relevant.</p></article>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="section" id="ownership">
|
||
<div class="section-head reveal">
|
||
<div><span class="section-number">02</span><h2>One owner per concern</h2></div>
|
||
<p>Maintenance gets easier when two plugins do not compete for the same surface. Select a row to inspect the boundary.</p>
|
||
</div>
|
||
<div class="ownership reveal">
|
||
<div class="owner-list" role="tablist" aria-label="Tool ownership" aria-orientation="vertical">
|
||
<div class="owner active" id="owner-format" role="tab" tabindex="0" aria-selected="true" aria-controls="detail-format" data-owner="format"><span class="owner-key">F</span><div><strong>Conform</strong><span>formatting</span></div></div>
|
||
<div class="owner" id="owner-lint" role="tab" tabindex="-1" aria-selected="false" aria-controls="detail-lint" data-owner="lint"><span class="owner-key">L</span><div><strong>nvim-lint</strong><span>diagnostics on save</span></div></div>
|
||
<div class="owner" id="owner-complete" role="tab" tabindex="-1" aria-selected="false" aria-controls="detail-complete" data-owner="complete"><span class="owner-key">C</span><div><strong>nvim-cmp + LuaSnip</strong><span>completion</span></div></div>
|
||
<div class="owner" id="owner-notify" role="tab" tabindex="-1" aria-selected="false" aria-controls="detail-notify" data-owner="notify"><span class="owner-key">N</span><div><strong>Snacks + Fidget + Noice</strong><span>three distinct UI channels</span></div></div>
|
||
<div class="owner" id="owner-files" role="tab" tabindex="-1" aria-selected="false" aria-controls="detail-files" data-owner="files"><span class="owner-key">E</span><div><strong>Snacks Explorer</strong><span>file browsing</span></div></div>
|
||
<div class="owner" id="owner-terminal" role="tab" tabindex="-1" aria-selected="false" aria-controls="detail-terminal" data-owner="terminal"><span class="owner-key">T</span><div><strong>ToggleTerm</strong><span>terminal sessions</span></div></div>
|
||
</div>
|
||
<div class="owner-detail">
|
||
<div class="active" id="detail-format" role="tabpanel" aria-labelledby="owner-format" data-detail="format"><span class="kicker">Deterministic output</span><h3>Formatters only.</h3><p>Conform runs explicit tools per filetype. Python prefers Ruff, with isort and Black as the fallback toolchain. LSP formatting is deliberately set to <code>never</code>, so a newly attached server cannot silently change save behavior.</p></div>
|
||
<div id="detail-lint" role="tabpanel" aria-labelledby="owner-lint" data-detail="lint"><span class="kicker">Diagnostics after write</span><h3>Lint without mutation.</h3><p>nvim-lint runs once on <code>BufWritePost</code>. Ruff supplies Python diagnostics. Pydoclint and Codespell join only when their executables exist, avoiding noisy missing-tool warnings.</p></div>
|
||
<div id="detail-complete" role="tabpanel" aria-labelledby="owner-complete" data-detail="complete"><span class="kicker">Insert-mode intelligence</span><h3>Completion needs no shim.</h3><p>nvim-cmp gathers sources and LuaSnip expands snippets. None-ls was not part of this path, so removing it does not remove completion.</p></div>
|
||
<div id="detail-notify" role="tabpanel" aria-labelledby="owner-notify" data-detail="notify"><span class="kicker">Clear channel boundaries</span><h3>Three surfaces, zero contention.</h3><p>Snacks owns general notifications. Fidget owns LSP progress. Noice owns the command line and message history, with its notification forwarding and LSP progress disabled.</p></div>
|
||
<div id="detail-files" role="tabpanel" aria-labelledby="owner-files" data-detail="files"><span class="kicker">One tree</span><h3>Explorer replaces two browsers.</h3><p><code><leader>fb</code> and <code><leader>nt</code> both open Snacks Explorer, preserving muscle memory while removing nvim-tree and Telescope file-browser.</p></div>
|
||
<div id="detail-terminal" role="tabpanel" aria-labelledby="owner-terminal" data-detail="terminal"><span class="kicker">Session-aware shells</span><h3>Named tools stay alive.</h3><p>ToggleTerm owns splits, tabs, floats, and named sessions for btop, IPython, iotop, Lazydocker, nvtop, and rmpc. Snacks Terminal remains explicitly disabled.</p></div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="section" id="keymaps">
|
||
<div class="section-head reveal">
|
||
<div><span class="section-number">03</span><h2>Keymaps follow ownership</h2></div>
|
||
<p>The configuration uses two mapping paths. The deciding question is whether the mapping requires a plugin.</p>
|
||
</div>
|
||
<div class="mapping-rule reveal">
|
||
<div class="rule-box"><h3>Core behavior</h3><p>Editing motions, workspace operations, LSP calls, and local helper commands use <code>vim.keymap.set</code> inside <code>lua/core/keymaps/</code>.</p></div>
|
||
<div class="rule-arrow" aria-hidden="true">◆</div>
|
||
<div class="rule-box"><h3>Plugin behavior</h3><p>Telescope, Snacks, CodeCompanion, images, Diffview, and terminals declare mappings in their Lazy <code>keys</code> tables. The mapping becomes the load trigger.</p></div>
|
||
</div>
|
||
<div class="split reveal" style="margin-top: 1px">
|
||
<article class="panel"><span class="kicker">Core example</span><h3>Direct and unsurprising</h3><pre><span class="lua-key">map</span>(<span class="lua-str">"n"</span>, <span class="lua-str">"<leader>ca"</span>, vim.lsp.buf.code_action, {
|
||
desc = <span class="lua-str">"Code action"</span>,
|
||
})</pre></article>
|
||
<article class="panel"><span class="kicker">Plugin example</span><h3>Mapping as load boundary</h3><pre>keys = {
|
||
{ <span class="lua-str">"<leader>fb"</span>, <span class="lua-key">function</span>()
|
||
Snacks.explorer()
|
||
<span class="lua-key">end</span>, desc = <span class="lua-str">"File browser"</span> },
|
||
}</pre></article>
|
||
</div>
|
||
<div class="callout reveal"><strong>which-key has one job:</strong> it labels prefix groups. It no longer converts mapping tables or acts as a second mapping registry.</div>
|
||
</section>
|
||
|
||
<section class="section" id="plugins">
|
||
<div class="section-head reveal">
|
||
<div><span class="section-number">04</span><h2>Lazy is the seam</h2></div>
|
||
<p>Every file in <code>lua/plugins/</code> returns one plugin spec. The spec keeps the trigger, dependencies, mappings, options, and exceptional setup in one navigable place.</p>
|
||
</div>
|
||
<div class="split reveal">
|
||
<article class="panel"><span class="kicker">Prefer data</span><h3><code>opts</code> for ordinary setup</h3><p>When a plugin follows <code>require(module).setup(opts)</code>, its configuration stays declarative. Lazy can merge it and call setup at the correct time.</p><pre><span class="lua-key">return</span> {
|
||
<span class="lua-str">"stevearc/conform.nvim"</span>,
|
||
opts = { <span class="lua-note">-- formatter policy</span> },
|
||
}</pre></article>
|
||
<article class="panel"><span class="kicker">Use code selectively</span><h3><code>config</code> for real orchestration</h3><p>ToggleTerm needs named terminal objects and a terminal-buffer autocmd. That lifecycle is real behavior, so a focused <code>config</code> function is justified.</p><pre>config = <span class="lua-key">function</span>(_, opts)
|
||
require(<span class="lua-str">"toggleterm"</span>).setup(opts)
|
||
<span class="lua-note">-- create named sessions once</span>
|
||
<span class="lua-key">end</span></pre></article>
|
||
</div>
|
||
<div class="callout reveal"><strong>Avoid eager requires:</strong> do not call plugin modules while Lua is merely building a spec table. Wrap callback references in a function so Lazy can finish registering the plugin before it loads.</div>
|
||
</section>
|
||
|
||
<section class="section" id="cleanup">
|
||
<div class="section-head reveal">
|
||
<div><span class="section-number">05</span><h2>Complexity removed</h2></div>
|
||
<p>The cleanup favors replacement over compatibility layers. Old aliases remain where useful, but dead modules and duplicate owners do not.</p>
|
||
</div>
|
||
<div class="removed reveal">
|
||
<article><del>nvim-tree</del><strong>Snacks Explorer</strong><p>One browser, two preserved entry mappings.</p></article>
|
||
<article><del>Telescope file-browser</del><strong>Telescope search</strong><p>Search stays focused on pickers and discovery.</p></article>
|
||
<article><del>nvim-notify</del><strong>Snacks Notifier</strong><p>General messages move to the plugin already loaded at startup.</p></article>
|
||
<article><del>none-ls</del><strong>Conform + nvim-lint</strong><p>Formatting, linting, and completion now have separate owners.</p></article>
|
||
<article><del>Opencode</del><strong>CodeCompanion</strong><p>One in-editor AI workflow remains, alongside Copilot completion.</p></article>
|
||
<article><del>converter modules</del><strong>Native mapping APIs</strong><p>Less indirection between a key and the behavior it invokes.</p></article>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="section" id="checks">
|
||
<div class="section-head reveal">
|
||
<div><span class="section-number">06</span><h2>Maintenance loop</h2></div>
|
||
<p>Run these from <code>~/.config/nvim</code>. Each check covers a different failure class: style drift, static mistakes, or startup integration.</p>
|
||
</div>
|
||
<div class="checks reveal">
|
||
<div class="check"><span class="check-state">01</span><code>stylua --check .</code><button class="copy" data-copy="stylua --check .">copy</button></div>
|
||
<div class="check"><span class="check-state">02</span><code>luacheck .</code><button class="copy" data-copy="luacheck .">copy</button></div>
|
||
<div class="check"><span class="check-state">03</span><code>jq empty lazy-lock.json</code><button class="copy" data-copy="jq empty lazy-lock.json">copy</button></div>
|
||
<div class="check"><span class="check-state">04</span><code>nvim --headless -i NONE -u ./init.lua '+qa'</code><button class="copy" data-copy="nvim --headless -i NONE -u ./init.lua '+qa'">copy</button></div>
|
||
</div>
|
||
<div class="callout reveal"><strong>Tool availability:</strong> optional linters should be conditional. The configuration registers Pydoclint and Codespell only when the corresponding executable is installed.</div>
|
||
</section>
|
||
|
||
<footer><span>Neovim Config Field Manual</span><span>Small core / explicit ownership / load on demand</span></footer>
|
||
</main>
|
||
</div>
|
||
|
||
<script>
|
||
const progress = document.querySelector(".reading-progress");
|
||
const sections = [...document.querySelectorAll("main section")];
|
||
const navLinks = [...document.querySelectorAll(".rail nav a")];
|
||
|
||
function updateScrollState() {
|
||
const max = document.documentElement.scrollHeight - innerHeight;
|
||
progress.style.width = `${max > 0 ? (scrollY / max) * 100 : 0}%`;
|
||
let current = sections[0]?.id;
|
||
for (const section of sections) {
|
||
if (section.getBoundingClientRect().top < innerHeight * 0.38) current = section.id;
|
||
}
|
||
for (const link of navLinks) link.classList.toggle("active", link.hash === `#${current}`);
|
||
}
|
||
|
||
addEventListener("scroll", updateScrollState, { passive: true });
|
||
updateScrollState();
|
||
|
||
const revealElements = document.querySelectorAll(".reveal");
|
||
if ("IntersectionObserver" in window) {
|
||
const observer = new IntersectionObserver((entries) => {
|
||
for (const entry of entries) {
|
||
if (entry.isIntersecting) {
|
||
entry.target.classList.add("visible");
|
||
observer.unobserve(entry.target);
|
||
}
|
||
}
|
||
}, { threshold: 0.12 });
|
||
revealElements.forEach((element) => observer.observe(element));
|
||
} else {
|
||
revealElements.forEach((element) => element.classList.add("visible"));
|
||
}
|
||
|
||
function selectOwner(owner) {
|
||
const name = owner.dataset.owner;
|
||
document.querySelectorAll(".owner").forEach((item) => {
|
||
const selected = item === owner;
|
||
item.classList.toggle("active", selected);
|
||
item.setAttribute("aria-selected", String(selected));
|
||
item.tabIndex = selected ? 0 : -1;
|
||
});
|
||
document.querySelectorAll("[data-detail]").forEach((detail) => detail.classList.toggle("active", detail.dataset.detail === name));
|
||
}
|
||
const owners = [...document.querySelectorAll(".owner")];
|
||
owners.forEach((owner, index) => {
|
||
owner.addEventListener("click", () => selectOwner(owner));
|
||
owner.addEventListener("keydown", (event) => {
|
||
if (event.key === "Enter" || event.key === " ") {
|
||
event.preventDefault();
|
||
selectOwner(owner);
|
||
}
|
||
if (["ArrowDown", "ArrowRight", "ArrowUp", "ArrowLeft", "Home", "End"].includes(event.key)) {
|
||
event.preventDefault();
|
||
const nextIndex = event.key === "Home" ? 0
|
||
: event.key === "End" ? owners.length - 1
|
||
: ["ArrowDown", "ArrowRight"].includes(event.key) ? (index + 1) % owners.length
|
||
: (index - 1 + owners.length) % owners.length;
|
||
selectOwner(owners[nextIndex]);
|
||
owners[nextIndex].focus();
|
||
}
|
||
});
|
||
});
|
||
|
||
async function copyText(text) {
|
||
if (navigator.clipboard?.writeText) {
|
||
try {
|
||
await navigator.clipboard.writeText(text);
|
||
return;
|
||
} catch {}
|
||
}
|
||
const field = document.createElement("textarea");
|
||
field.value = text;
|
||
field.setAttribute("readonly", "");
|
||
field.style.position = "fixed";
|
||
field.style.opacity = "0";
|
||
document.body.append(field);
|
||
field.select();
|
||
const copied = document.execCommand("copy");
|
||
field.remove();
|
||
if (!copied) throw new Error("Copy is unavailable");
|
||
}
|
||
|
||
document.querySelectorAll(".copy").forEach((button) => {
|
||
button.setAttribute("aria-live", "polite");
|
||
button.addEventListener("click", async () => {
|
||
try {
|
||
await copyText(button.dataset.copy);
|
||
button.textContent = "copied";
|
||
} catch {
|
||
button.textContent = "copy failed";
|
||
}
|
||
setTimeout(() => { button.textContent = "copy"; }, 1200);
|
||
});
|
||
});
|
||
</script>
|
||
</body>
|
||
</html>
|