<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Yuexun J</title><description>Building tools. Writing about web tech, Vim, Rust, and whatever else I learn along the way.</description><link>https://yuexunj.com/</link><item><title>Developing an App with My AI Intern</title><link>https://yuexunj.com/developing-an-app-with-my-ai-intern/</link><guid isPermaLink="true">https://yuexunj.com/developing-an-app-with-my-ai-intern/</guid><description>AI won&apos;t replace programmers. But it taught me Rust. The best tools don&apos;t do the work for you — they make you better at doing it yourself.</description><pubDate>Wed, 24 Jul 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I got scolded for missing a message. That&apos;s how this started.&lt;/p&gt;
&lt;p&gt;Since moving abroad, Messages became my main IM. Problem is, Messages doesn&apos;t have a menu bar icon. I hide my Dock. You see where this is going.&lt;/p&gt;
&lt;p&gt;I found &lt;a href=&quot;https://github.com/xiaogdgenuine/Doll&quot;&gt;Doll&lt;/a&gt;, an app that puts notification badges in the menu bar. It worked. But the icon rendering was ugly — just color inversion that looked wrong on different backgrounds.&lt;/p&gt;
&lt;p&gt;I could live with it. Or I could build something better.&lt;/p&gt;
&lt;p&gt;I chose the latter. We shipped it as &lt;a href=&quot;https://badgeify.app/&quot;&gt;Badgeify&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;The Intern&lt;/h2&gt;
&lt;p&gt;Electron felt like bringing a semi-truck to a bicycle race. Tauri was the obvious choice. One problem: Tauri means Rust, and I didn&apos;t know Rust.&lt;/p&gt;
&lt;p&gt;Enter Claude 3.5 Sonnet. My AI intern.&lt;/p&gt;
&lt;p&gt;I call it an intern deliberately. It&apos;s knowledgeable. It&apos;s fast. It&apos;s occasionally wrong. You don&apos;t trust an intern blindly. You guide them.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../assets/blog/developing-an-app-with-my-ai-intern/ai-intern-functions.png&quot; alt=&quot;Some small functions written by my AI intern&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The division of labor was simple: I handled the frontend. The intern handled the Rust parts I didn&apos;t understand — macOS Accessibility APIs, SVG-to-PNG conversion, Dock status monitoring.&lt;/p&gt;
&lt;p&gt;Most of its code didn&apos;t work on first try. Wrong parameters. Deprecated APIs. I&apos;d point it to GitHub repos, give it hints, let it iterate. Eventually, things ran.&lt;/p&gt;
&lt;h2&gt;Where AI Falls Short&lt;/h2&gt;
&lt;p&gt;One task: monitor menu bar background color changes. The intern produced elegant code. Clean abstractions. Proper API usage.&lt;/p&gt;
&lt;p&gt;It missed every edge case that mattered.&lt;/p&gt;
&lt;p&gt;We scrapped it and went with a completely different approach. My approach. This is the gap that matters — AI writes syntactically correct code. It doesn&apos;t understand what the code is &lt;em&gt;for&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;When the intern got stuck in loops, I stepped in. I learned Rust on the fly. Not from tutorials. From fixing the intern&apos;s mistakes.&lt;/p&gt;
&lt;p&gt;By the end, I could write most of the Rust myself.&lt;/p&gt;
&lt;p&gt;Thanks for teaching me Rust, intern.&lt;/p&gt;
&lt;h2&gt;Shipping&lt;/h2&gt;
&lt;p&gt;A few weeks later, the MVP was live on Reddit.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../assets/blog/developing-an-app-with-my-ai-intern/menu-bar-icons.jpeg&quot; alt=&quot;Adding some app icons to the menu bar&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The image processing logic? Written by the intern, refined by me. It works beautifully now.&lt;/p&gt;
&lt;p&gt;I kept the intern busy after launch. Custom icon uploads. Code review suggestions. Some suggestions were decent. Most weren&apos;t. I ignored them.&lt;/p&gt;
&lt;h2&gt;Will AI Replace Programmers?&lt;/h2&gt;
&lt;p&gt;No.&lt;/p&gt;
&lt;p&gt;If it could, I would&apos;ve shipped this app in a weekend. Instead, it took weeks. The intern couldn&apos;t handle deployment, infrastructure, product decisions. It couldn&apos;t handle the parts that actually matter.&lt;/p&gt;
&lt;p&gt;Here&apos;s what I&apos;ve learned: AI is a Swiss Army knife. Lots of tools. None of them are the right tool for every job. Sometimes the best solution is to close the AI chat and think.&lt;/p&gt;
&lt;p&gt;The future isn&apos;t AI replacing programmers. It&apos;s programmers with AI interns. Those who can&apos;t outperform the intern will have a problem. Everyone else will be fine.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;AI didn&apos;t replace me. It couldn&apos;t.&lt;/p&gt;
&lt;p&gt;But it taught me Rust, and for that, I&apos;m grateful.&lt;/p&gt;
&lt;p&gt;The best tools don&apos;t do the work for you — they make you better at doing it yourself.&lt;/p&gt;
</content:encoded></item><item><title>Why MCP Matters</title><link>https://yuexunj.com/why-mcp-matters/</link><guid isPermaLink="true">https://yuexunj.com/why-mcp-matters/</guid><description>MCP is not just another API standard. It&apos;s the first protocol genuinely designed for AI agents, and its evolution from STDIO to OAuth proves its value.</description><pubDate>Sun, 11 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&quot;MCP is a fad.&quot; I&apos;ve seen this take floating around. Too complex. Security holes everywhere. Just another wrapper around function calls.&lt;/p&gt;
&lt;p&gt;I disagree.&lt;/p&gt;
&lt;h2&gt;The Problem No One Solved&lt;/h2&gt;
&lt;p&gt;Every protocol we use today was designed with the same assumption: a human sits in the middle. REST. GraphQL. gRPC. A human reads the docs. A human writes the integration. A human debugs when things break.&lt;/p&gt;
&lt;p&gt;Agents don&apos;t work that way.&lt;/p&gt;
&lt;p&gt;They can read documentation, sure. But they can&apos;t interpret what&apos;s missing from it. They need structured, machine-readable contracts. Capabilities they can discover at runtime. Error semantics they can actually parse.&lt;/p&gt;
&lt;p&gt;This is what MCP actually solves. Not &quot;a better way to call functions.&quot; The first protocol that treats agents as first-class citizens.&lt;/p&gt;
&lt;h2&gt;A Different Mental Model&lt;/h2&gt;
&lt;p&gt;The obvious answer is &quot;tools for AI.&quot; That&apos;s not it.&lt;/p&gt;
&lt;p&gt;MCP is bidirectional. Servers can request LLM completions from the host. The tool asks the agent for help. This isn&apos;t RPC. It&apos;s a conversation. REST can&apos;t express this pattern at all.&lt;/p&gt;
&lt;p&gt;MCP is resource-aware. Instead of an agent repeatedly calling APIs and parsing responses (wasting tokens every time), it exposes structured data directly. Schemas, documentation, configs. Load once, work with it.&lt;/p&gt;
&lt;p&gt;MCP is dynamic. Client and server negotiate capabilities when they connect. The agent doesn&apos;t need prior knowledge of every tool. It discovers them.&lt;/p&gt;
&lt;p&gt;These aren&apos;t incremental improvements over REST. They&apos;re a different way of thinking about how software talks to AI.&lt;/p&gt;
&lt;h2&gt;STDIO Was Never the Point&lt;/h2&gt;
&lt;p&gt;MCP launched with STDIO transport. Client spawns server as a subprocess. Communication happens through stdin and stdout. Dead simple.&lt;/p&gt;
&lt;p&gt;Critics love pointing out the limitations. Single client. No authentication. Local only. Tightly coupled process lifecycles. All valid criticisms.&lt;/p&gt;
&lt;p&gt;But they miss the point.&lt;/p&gt;
&lt;p&gt;STDIO was the MVP. It proved one thing: Claude Desktop could call local tools through a standardized protocol. That&apos;s all it needed to prove.&lt;/p&gt;
&lt;p&gt;HTTP 0.9 had no headers, no status codes, no content types. The first version of any protocol exists to validate the idea. Evolution comes after.&lt;/p&gt;
&lt;h2&gt;OAuth Is Where It Gets Real&lt;/h2&gt;
&lt;p&gt;The introduction of Streamable HTTP and OAuth 2.1 transformed MCP from a local hack into genuine infrastructure.&lt;/p&gt;
&lt;p&gt;Streamable HTTP unlocks what STDIO couldn&apos;t. Multi-client servers. Session management. Disconnection recovery. An MCP server can now run as a proper service, deployed independently, serving multiple agents at once.&lt;/p&gt;
&lt;p&gt;OAuth solves the trust problem that STDIO ignored entirely. When an agent reads your email or accesses your calendar, it&apos;s acting on your behalf. That requires proper consent flows. Token binding to specific resources. Step-up authorization when the agent needs elevated permissions.&lt;/p&gt;
&lt;p&gt;This is what an agent ecosystem actually needs. Not local script execution. Secure, authorized access to user data across services.&lt;/p&gt;
&lt;h2&gt;Evolution as Feature&lt;/h2&gt;
&lt;p&gt;Some criticize MCP for changing. For having limitations early on. For not shipping perfect on day one.&lt;/p&gt;
&lt;p&gt;I see it differently.&lt;/p&gt;
&lt;p&gt;A protocol&apos;s value isn&apos;t measured by its initial state. It&apos;s measured by its capacity to evolve. MCP now has formal governance: &lt;a href=&quot;https://github.com/modelcontextprotocol/specification&quot;&gt;Specification Enhancement Proposals&lt;/a&gt;, Working Groups, oversight from the Linux Foundation. The community is actively tackling real problems:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/modelcontextprotocol/specification/pull/1391&quot;&gt;Async tasks&lt;/a&gt; for operations that take minutes, not milliseconds&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/modelcontextprotocol/specification/issues/1821&quot;&gt;Dynamic tool discovery&lt;/a&gt; so agents aren&apos;t overwhelmed by large catalogs&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/modelcontextprotocol/specification/issues/1415&quot;&gt;HTTP message signing&lt;/a&gt; for cryptographic authentication&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The protocol keeps improving. That&apos;s exactly the point.&lt;/p&gt;
&lt;h2&gt;Honest Tradeoffs&lt;/h2&gt;
&lt;p&gt;MCP isn&apos;t universally necessary. I should be upfront about that.&lt;/p&gt;
&lt;p&gt;For coding agents with direct filesystem and shell access, the value proposition gets murky. These agents write their own glue code. They&apos;re often good at it.&lt;/p&gt;
&lt;p&gt;But think about web-based assistants. ChatGPT. Claude.ai. They can&apos;t spawn subprocesses or touch local files. They need a standardized way to connect to external services: your calendar, your email, your company&apos;s internal tools.&lt;/p&gt;
&lt;p&gt;Here, MCP provides genuine value. One integration that works across every AI platform.&lt;/p&gt;
&lt;p&gt;For enterprises, this translates to real savings. Expose MCP as a connector layer on top of existing APIs. Write the integration once, support every AI client. Not technical elegance. Ecosystem efficiency.&lt;/p&gt;
&lt;h2&gt;The Bet&lt;/h2&gt;
&lt;p&gt;Will MCP become the standard? I don&apos;t know.&lt;/p&gt;
&lt;p&gt;But the protocols we have were built for humans. Agents need their own. Someone has to build it.&lt;/p&gt;
&lt;p&gt;That&apos;s why MCP matters.&lt;/p&gt;
</content:encoded></item><item><title>How the Notion Editor Works</title><link>https://yuexunj.com/how-the-notion-editor-works/</link><guid isPermaLink="true">https://yuexunj.com/how-the-notion-editor-works/</guid><description>I dug into Notion&apos;s code. Here&apos;s what I found — block-based architecture, custom selection handling, and some surprising shortcuts.</description><pubDate>Thu, 25 Mar 2021 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;Written in December 2020. Some details may be outdated.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Notion is everywhere. Note-taking, knowledge management, project management — all in one tool.&lt;/p&gt;
&lt;p&gt;I&apos;ve used it for years. But I never looked at &lt;em&gt;how&lt;/em&gt; it works. The editor, specifically.&lt;/p&gt;
&lt;p&gt;So I opened DevTools, dug through the minified code, and figured it out.&lt;/p&gt;
&lt;p&gt;Here&apos;s what I found.&lt;/p&gt;
&lt;h2&gt;TL;DR&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Everything is a Block. Blocks are independent. Some have editable regions, some don&apos;t.&lt;/li&gt;
&lt;li&gt;Layout uses CSS Flexbox. Simple, but limited.&lt;/li&gt;
&lt;li&gt;Editable regions use &lt;code&gt;contenteditable&lt;/code&gt; divs. But Notion doesn&apos;t use &lt;code&gt;document.execCommand&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;React renders the view, but Notion implements its own render queue.&lt;/li&gt;
&lt;li&gt;Every edit creates a &lt;code&gt;transaction&lt;/code&gt; with &lt;code&gt;operations&lt;/code&gt;. Operations modify block data.&lt;/li&gt;
&lt;li&gt;Text formatting is stored as structured data, not HTML. The editor rebuilds DOM from data.&lt;/li&gt;
&lt;li&gt;Selection is custom. Cross-block selection triggers block-level highlighting, not text selection.&lt;/li&gt;
&lt;li&gt;Copy-paste is inconsistent. Single-line pastes lose formatting. Multi-line pastes parse HTML.&lt;/li&gt;
&lt;li&gt;Undo/redo uses a stack with inverted operations.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Editable Regions&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;../../assets/blog/how-the-notion-editor-works/I0lEI8.png&quot; alt=&quot;contenteditable implementation&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Each text block is a &lt;code&gt;contenteditable&lt;/code&gt; div. Independent from other blocks.&lt;/p&gt;
&lt;p&gt;This means cross-block selection is tricky. You can&apos;t just drag across multiple blocks like in Google Docs.&lt;/p&gt;
&lt;p&gt;Non-text blocks (images, embeds) are just DOM. No &lt;code&gt;contenteditable&lt;/code&gt;. No cursor logic needed.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../assets/blog/how-the-notion-editor-works/GypIyz.png&quot; alt=&quot;DOM rendering&quot; /&gt;&lt;/p&gt;
&lt;p&gt;This separation simplifies things. No zero-width characters for cursor positioning. Each block handles its own editing.&lt;/p&gt;
&lt;h2&gt;Layout&lt;/h2&gt;
&lt;p&gt;Notion&apos;s layout is basic. Flexbox.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../assets/blog/how-the-notion-editor-works/L9s6Tv.png&quot; alt=&quot;Flexbox layout&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Drag two blocks side by side, they split evenly. Resize, and the width ratio saves to block data.&lt;/p&gt;
&lt;p&gt;Want right-aligned content? Add an empty block on the left and adjust widths.&lt;/p&gt;
&lt;p&gt;It works. But complex layouts? Not really possible.&lt;/p&gt;
&lt;h2&gt;Text Input&lt;/h2&gt;
&lt;p&gt;When you type, Notion listens to events on the &lt;code&gt;contenteditable&lt;/code&gt; div. But it doesn&apos;t let the browser handle the edit.&lt;/p&gt;
&lt;p&gt;Instead:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Event fires&lt;/li&gt;
&lt;li&gt;Notion creates a &lt;code&gt;transaction&lt;/code&gt; with &lt;code&gt;operations&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Operations modify block data&lt;/li&gt;
&lt;li&gt;View re-renders from data&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Notion uses React, but not React&apos;s normal rendering. It maintains a custom queue. Components call &lt;code&gt;forceUpdate&lt;/code&gt;, Notion batches and executes them.&lt;/p&gt;
&lt;p&gt;Every edit produces a transaction:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../assets/blog/how-the-notion-editor-works/pLjnQG.png&quot; alt=&quot;Transaction structure&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Type &quot;4&quot; after &quot;123&quot;? The transaction contains a &lt;code&gt;set&lt;/code&gt; operation that updates the block&apos;s &lt;code&gt;properties.title&lt;/code&gt; to &quot;1234&quot;.&lt;/p&gt;
&lt;p&gt;The entire block value is replaced. Every keystroke.&lt;/p&gt;
&lt;p&gt;This means long blocks get slow. That&apos;s why pressing Enter always creates a new block.&lt;/p&gt;
&lt;h3&gt;Line Breaks Create Blocks&lt;/h3&gt;
&lt;p&gt;Press Enter, Notion intercepts it. Creates a new block. Moves focus.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../assets/blog/how-the-notion-editor-works/SsC8ot.png&quot; alt=&quot;Block creation operations&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Four operations:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;set&lt;/code&gt; — create new block with fresh ID&lt;/li&gt;
&lt;li&gt;&lt;code&gt;update&lt;/code&gt; — link to parent document&lt;/li&gt;
&lt;li&gt;&lt;code&gt;listAfter&lt;/code&gt; — position after current block&lt;/li&gt;
&lt;li&gt;&lt;code&gt;set&lt;/code&gt; — initialize with empty content&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Search for &quot;commit&quot; or &quot;createAndCommit&quot; in Notion&apos;s code to see transactions in action.&lt;/p&gt;
&lt;h2&gt;Formatting&lt;/h2&gt;
&lt;p&gt;Formatting works similarly. Intercept keyboard event, generate transaction, update data.&lt;/p&gt;
&lt;p&gt;Notion listens globally for key combinations. &lt;code&gt;Cmd+B&lt;/code&gt; for bold, etc.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../assets/blog/how-the-notion-editor-works/TNj7rw.png&quot; alt=&quot;Formatting call stack&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The transaction:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../assets/blog/how-the-notion-editor-works/GK0GJO.png&quot; alt=&quot;Formatting data structure&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Bold &quot;bold&quot; in &quot;this is a bold text&quot; produces:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[[&quot;this is a &quot;], [&quot;bold&quot;, [[&quot;b&quot;]]], [&quot; text&quot;]]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The format: &lt;code&gt;[[TEXT, [[FORMAT], [FORMAT]]], [TEXT], ...]&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Formatting metadata lives with the text. Not in HTML attributes.&lt;/p&gt;
&lt;p&gt;This avoids &lt;code&gt;document.execCommand&lt;/code&gt; entirely. Better cross-browser consistency.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../assets/blog/how-the-notion-editor-works/kVFN6O.png&quot; alt=&quot;Format markers&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;b&lt;/code&gt; for bold, &lt;code&gt;i&lt;/code&gt; for italic, &lt;code&gt;a&lt;/code&gt; for links, and so on.&lt;/p&gt;
&lt;h2&gt;Selection&lt;/h2&gt;
&lt;p&gt;Inside a block: browser&apos;s native selection.&lt;/p&gt;
&lt;p&gt;Across blocks: Notion takes over.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i.imgur.com/5ndGfme.gif&quot; alt=&quot;Block selection&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Drag beyond a block&apos;s boundary, and Notion adds &lt;code&gt;div.notion-selectable-halo&lt;/code&gt; to highlight the entire block. Continue dragging, more blocks highlight.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../assets/blog/how-the-notion-editor-works/dESGqk.png&quot; alt=&quot;Selection highlight&quot; /&gt;&lt;/p&gt;
&lt;p&gt;It&apos;s not true text selection. It&apos;s block selection.&lt;/p&gt;
&lt;p&gt;There&apos;s also a quirk: box selection behavior changes based on x-position at the same y-coordinate.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i.imgur.com/I9rTKnn.gif&quot; alt=&quot;Selection quirk&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Not intuitive.&lt;/p&gt;
&lt;h2&gt;Copy-Paste&lt;/h2&gt;
&lt;p&gt;Notion handles multiple formats:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;text/plain&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;text/html&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;text/uri-list&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;text/_notion-blocks-v2-production&lt;/code&gt; (internal)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;text/_notion-text-production&lt;/code&gt; (internal)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;../../assets/blog/how-the-notion-editor-works/9sBc1t.png&quot; alt=&quot;Paste handling&quot; /&gt;&lt;/p&gt;
&lt;p&gt;External paste logic:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Multi-line? Parse &lt;code&gt;text/html&lt;/code&gt;, restore formatting.&lt;/li&gt;
&lt;li&gt;Single-line? Treat as plain text. &lt;strong&gt;All formatting lost.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is a significant limitation. Paste one styled line, you get plain text.&lt;/p&gt;
&lt;h3&gt;Internal Block Copy&lt;/h3&gt;
&lt;p&gt;Copy a block in Notion, and the clipboard gets:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;An HTML link placeholder&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;text/_notion-blocks-v2-production&lt;/code&gt; JSON blob&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;../../assets/blog/how-the-notion-editor-works/68AJms.png&quot; alt=&quot;Internal copy&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Pasting creates a reference, then fetches block data via API (&lt;code&gt;api/v3/syncRecordValues&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;This means &lt;strong&gt;internal block paste requires network&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../assets/blog/how-the-notion-editor-works/4AkSpw.png&quot; alt=&quot;Network dependency&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Offline web? Block paste fails. Mobile apps cache data locally, so they work offline.&lt;/p&gt;
&lt;h3&gt;Internal Text Copy&lt;/h3&gt;
&lt;p&gt;Text copy is simpler. Full data lives in &lt;code&gt;text/_notion-text-production&lt;/code&gt;. No network needed.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../assets/blog/how-the-notion-editor-works/yP1jnr.png&quot; alt=&quot;Text copy&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Undo-Redo&lt;/h2&gt;
&lt;p&gt;Every transaction includes &lt;code&gt;invertedOperations&lt;/code&gt; — the reverse of what was done.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../assets/blog/how-the-notion-editor-works/vqaPgB.png&quot; alt=&quot;Undo-redo stack&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Notion maintains a &lt;code&gt;revisionStack&lt;/code&gt;. Pointer at the top.&lt;/p&gt;
&lt;p&gt;Undo: execute &lt;code&gt;invertedOperations&lt;/code&gt; from current transaction, move pointer down.&lt;/p&gt;
&lt;p&gt;Redo: execute &lt;code&gt;operations&lt;/code&gt; from next transaction, move pointer up.&lt;/p&gt;
&lt;p&gt;Edit after undo: slice the stack, push new transaction, pointer to top.&lt;/p&gt;
&lt;p&gt;Standard undo-redo, cleanly implemented.&lt;/p&gt;
&lt;h2&gt;The Takeaway&lt;/h2&gt;
&lt;p&gt;Notion&apos;s block-based architecture is elegant. Every row in a database is a block. Every embed is a block. Views (kanban, calendar, timeline) are just different renderings of the same blocks.&lt;/p&gt;
&lt;p&gt;Extensibility is built in. Third-party embeds fit naturally.&lt;/p&gt;
&lt;p&gt;But the details reveal rough edges:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Single-line paste loses formatting&lt;/li&gt;
&lt;li&gt;Internal paste needs network&lt;/li&gt;
&lt;li&gt;Selection behavior is inconsistent&lt;/li&gt;
&lt;li&gt;Long blocks get slow&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The foundation is solid. The execution has room to grow.&lt;/p&gt;
&lt;p&gt;I might dig deeper later. There&apos;s more to find.&lt;/p&gt;
</content:encoded></item><item><title>My Journey with Vim</title><link>https://yuexunj.com/my-journey-with-vim/</link><guid isPermaLink="true">https://yuexunj.com/my-journey-with-vim/</guid><description>Does Vim make me faster? Probably not. Does it matter? Not at all. The joy is in the tinkering. Seven years in, and I&apos;m still not done.</description><pubDate>Thu, 09 May 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Seven years with Vim. Still going.&lt;/p&gt;
&lt;p&gt;Back then, I was using Sublime Text and Atom. Atom was popular. Hard to imagine now.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../assets/blog/my-journey-with-vim/my-initial-vim-configuration.png&quot; alt=&quot;My initial Vim configuration &quot; /&gt;&lt;/p&gt;
&lt;h2&gt;The Spark&lt;/h2&gt;
&lt;p&gt;Second year of college. Internship. My mentor Doni was an Emacs user. He watched me hammer the arrow keys in Atom and looked at me like I was personally offending him.&lt;/p&gt;
&lt;p&gt;So I tried Emacs. It wasn&apos;t for me.&lt;/p&gt;
&lt;p&gt;Then I found Vim. That was it.&lt;/p&gt;
&lt;p&gt;I started with a basic &lt;code&gt;.vimrc&lt;/code&gt; from some tutorial. Mapped &lt;code&gt;H/L&lt;/code&gt; to &lt;code&gt;^/$&lt;/code&gt;. Made Space my leader key. Probably made a dozen decisions I&apos;d question later.&lt;/p&gt;
&lt;p&gt;Here&apos;s the thing: I still question some of those mappings. Every time I SSH into a server and realize vanilla vi doesn&apos;t work the way my fingers expect, I wonder if I trained myself wrong.&lt;/p&gt;
&lt;p&gt;But they work on my machine. That&apos;s what matters.&lt;/p&gt;
&lt;h2&gt;The Obsession&lt;/h2&gt;
&lt;p&gt;GitHub became my source of inspiration. I&apos;d browse other people&apos;s dotfiles for hours, stealing ideas, tweaking endlessly.&lt;/p&gt;
&lt;p&gt;One &lt;code&gt;.vimrc&lt;/code&gt; file became many. Mappings in one file. Options in another. Plugins managed through &lt;a href=&quot;https://github.com/junegunn/vim-plug&quot;&gt;vim-plug&lt;/a&gt;. I wrote scripts to automate installation. Pushed everything to GitHub. When my laptop died once, I had a new dev environment running in minutes.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Plug &apos;mbbill/undotree&apos;
Plug &apos;scrooloose/nerdtree&apos;
Plug &apos;w0rp/ale&apos;
Plug &apos;junegunn/fzf.vim&apos;
Plug &apos;Valloric/YouCompleteMe&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I don&apos;t use most of these anymore. Tools come and go. The obsession stays.&lt;/p&gt;
&lt;p&gt;Completion plugins alone: YouCompleteMe (slow, painful to install), then deoplete (lighter), then &lt;a href=&quot;https://github.com/neoclide/coc.nvim&quot;&gt;coc.nvim&lt;/a&gt; (finally, something that just worked).&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../assets/blog/my-journey-with-vim/the-plugins-what-i-created.png&quot; alt=&quot;The plugins what I created&quot; /&gt;&lt;/p&gt;
&lt;p&gt;I even wrote my own plugins. &lt;a href=&quot;https://github.com/ahonn/resize.vim&quot;&gt;resize.vim&lt;/a&gt; — because I hated using a mouse to resize windows. &lt;a href=&quot;https://github.com/ahonn/vim-fileheader&quot;&gt;vim-fileheader&lt;/a&gt; — auto-generates file headers with my name and timestamps.&lt;/p&gt;
&lt;p&gt;Looking back, the file header thing is kind of embarrassing. I haven&apos;t cared about putting my name in files for years. But building those plugins? Pure joy. Even if Vimscript made me want to quit.&lt;/p&gt;
&lt;h2&gt;Neovim&lt;/h2&gt;
&lt;p&gt;At some point — I can&apos;t remember exactly when — I switched to Neovim.&lt;/p&gt;
&lt;p&gt;Float windows. Built-in LSP. Lua instead of Vimscript. It felt like the future.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../assets/blog/my-journey-with-vim/my-configuration-is-fully-lua-based.png&quot; alt=&quot;My configuration is fully lua-based for now&quot; /&gt;&lt;/p&gt;
&lt;p&gt;I rewrote everything in Lua. Years of Vimscript, gone. No regrets.&lt;/p&gt;
&lt;p&gt;Now I run &lt;a href=&quot;https://github.com/folke/lazy.nvim&quot;&gt;lazy.nvim&lt;/a&gt; for plugins. &lt;a href=&quot;https://github.com/nvim-telescope/telescope.nvim&quot;&gt;telescope.nvim&lt;/a&gt; for fuzzy finding. &lt;a href=&quot;https://github.com/nvim-neo-tree/neo-tree.nvim&quot;&gt;neo-tree.nvim&lt;/a&gt; for files. &lt;a href=&quot;https://github.com/nvim-treesitter/nvim-treesitter&quot;&gt;nvim-treesitter&lt;/a&gt; for highlighting. &lt;a href=&quot;https://github.com/hrsh7th/nvim-cmp&quot;&gt;nvim-cmp&lt;/a&gt; with LSP for everything else.&lt;/p&gt;
&lt;p&gt;That&apos;s the stack. It&apos;ll probably change again. Everything does.&lt;/p&gt;
&lt;p&gt;If you&apos;re curious: &lt;a href=&quot;https://github.com/ahonn/dotfiles/tree/master/config/nvim/lua/config/plugins&quot;&gt;my dotfiles&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;The Point&lt;/h2&gt;
&lt;p&gt;Does Vim make me faster?&lt;/p&gt;
&lt;p&gt;Probably not.&lt;/p&gt;
&lt;p&gt;Does it matter?&lt;/p&gt;
&lt;p&gt;Not at all.&lt;/p&gt;
&lt;p&gt;The joy is in the tinkering. The endless configuration. The rabbit holes. The moment something finally clicks and your editor does exactly what you imagined.&lt;/p&gt;
&lt;p&gt;I&apos;ll probably rewrite my config again soon. I always do.&lt;/p&gt;
&lt;p&gt;Seven years in, and I&apos;m still not done. That&apos;s the point.&lt;/p&gt;
</content:encoded></item><item><title>How to Make Your Tauri Dev Faster</title><link>https://yuexunj.com/how-to-make-your-tauri-dev-faster/</link><guid isPermaLink="true">https://yuexunj.com/how-to-make-your-tauri-dev-faster/</guid><description>A one-minute compile became fifteen seconds. Here&apos;s what was wrong and how to fix it.</description><pubDate>Thu, 01 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Tauri is great. Small bundles. Low memory. Not Electron.&lt;/p&gt;
&lt;p&gt;But &lt;code&gt;tauri dev&lt;/code&gt; can be painfully slow. Change one line of Rust, wait a minute. Watch dependencies recompile for no apparent reason. Wonder if you made a terrible technology choice.&lt;/p&gt;
&lt;p&gt;You didn&apos;t. The tooling is just fighting itself.&lt;/p&gt;
&lt;p&gt;Here&apos;s how to fix it.&lt;/p&gt;
&lt;h2&gt;The Problem&lt;/h2&gt;
&lt;p&gt;Watch your terminal next time you run &lt;code&gt;tauri dev&lt;/code&gt;. Notice how it recompiles dependencies that haven&apos;t changed? That&apos;s not normal. Something is triggering unnecessary rebuilds.&lt;/p&gt;
&lt;p&gt;The culprit: &lt;code&gt;MACOSX_DEPLOYMENT_TARGET&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Tauri reads &lt;code&gt;bundle.macos.minimumSystemVersion&lt;/code&gt; from your config and sets this environment variable during builds. If you haven&apos;t set it, it defaults to &lt;code&gt;10.13&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Meanwhile, &lt;code&gt;rust-analyzer&lt;/code&gt; runs &lt;code&gt;cargo check&lt;/code&gt; in your editor — without that variable set. Different environment means different build cache. Every time you save, both tools invalidate each other&apos;s work.&lt;/p&gt;
&lt;p&gt;One minute per change. Unacceptable.&lt;/p&gt;
&lt;h2&gt;The Fix&lt;/h2&gt;
&lt;p&gt;Make them agree. Add this to your editor settings:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;rust-analyzer.cargo.extraEnv&quot;: {
    &quot;MACOSX_DEPLOYMENT_TARGET&quot;: &quot;10.13&quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And this to &lt;code&gt;tauri.conf.json&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;bundle&quot;: {
    &quot;macos&quot;: {
      &quot;minimumSystemVersion&quot;: &quot;10.13&quot;
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Same value. Both places. Restart everything.&lt;/p&gt;
&lt;p&gt;One minute became twenty-five seconds.&lt;/p&gt;
&lt;h2&gt;Still Slow?&lt;/h2&gt;
&lt;p&gt;You might still see this: &lt;code&gt;Blocking waiting for file lock on build directory&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;rust-analyzer&lt;/code&gt; and &lt;code&gt;tauri dev&lt;/code&gt; are fighting over the same &lt;code&gt;target&lt;/code&gt; folder. One waits for the other. Your build stalls.&lt;/p&gt;
&lt;p&gt;Give them separate directories:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;rust-analyzer.cargo.targetDir&quot;: &quot;target/analyzer&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now &lt;code&gt;rust-analyzer&lt;/code&gt; writes to &lt;code&gt;target/analyzer&lt;/code&gt;. &lt;code&gt;tauri dev&lt;/code&gt; writes to &lt;code&gt;target&lt;/code&gt;. No conflicts.&lt;/p&gt;
&lt;p&gt;This also fixes the environment variable problem. Separate caches mean they can&apos;t invalidate each other.&lt;/p&gt;
&lt;p&gt;Twenty-five seconds became fifteen.&lt;/p&gt;
&lt;h2&gt;Going Further&lt;/h2&gt;
&lt;p&gt;Want more? Tweak your Cargo profiles.&lt;/p&gt;
&lt;p&gt;You probably don&apos;t need full debug info for dependencies. A little optimization can speed up linking.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# src-tauri/Cargo.toml

[profile.dev]
incremental = true
opt-level = 0
debug = true

[profile.dev.package.&quot;*&quot;]
opt-level = 1
debug = false
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;First build after this change will be slow. Dependencies need recompiling. Judge by subsequent builds.&lt;/p&gt;
&lt;p&gt;Fifteen seconds became ten.&lt;/p&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;Three changes:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Match &lt;code&gt;MACOSX_DEPLOYMENT_TARGET&lt;/code&gt;&lt;/strong&gt; — Same value in editor settings and &lt;code&gt;tauri.conf.json&lt;/code&gt;. Stops cache invalidation between tools.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Set &lt;code&gt;rust-analyzer.cargo.targetDir&lt;/code&gt;&lt;/strong&gt; — Separate build caches. No file locks. No environment conflicts. This is the big one.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Optimize dependency builds&lt;/strong&gt; — Less debug info, slight optimization. Minor gains, some trade-offs.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;One minute to ten seconds. Worth the five minutes it took to configure.&lt;/p&gt;
&lt;p&gt;Now get back to building.&lt;/p&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/tauri-apps/tauri/issues/8920&quot;&gt;tauri dev is incredibly slow&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/rust-lang/rust-analyzer/issues/4616&quot;&gt;Rust analyzer &quot;cargo check&quot; blocks debug builds&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/tauri-apps/tauri/issues/11577&quot;&gt;Tauri unilaterally overrides MACOSX_DEPLOYMENT_TARGET&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/rust-lang/rust-analyzer/issues/6007&quot;&gt;Option to use rust-analyzer-specific target directory&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://doc.rust-lang.org/cargo/reference/profiles.html#default-profiles&quot;&gt;Profiles - The Cargo Book&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>The Vim Guide for VS Code Users</title><link>https://yuexunj.com/the-vim-guide-for-vs-code-users/</link><guid isPermaLink="true">https://yuexunj.com/the-vim-guide-for-vs-code-users/</guid><description>You don&apos;t have to abandon VS Code to learn Vim. The editing model is what matters. Here&apos;s everything you need to start.</description><pubDate>Tue, 29 Sep 2020 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Arrow keys are slow. You just don&apos;t know it yet.&lt;/p&gt;
&lt;p&gt;Most frontend developers use VS Code. Almost none use Vim. But Vim&apos;s editing model is worth learning regardless of your editor. Once you internalize it, reaching for arrow keys feels like walking through mud.&lt;/p&gt;
&lt;p&gt;You don&apos;t have to switch editors. VS Code has excellent Vim support. What matters is the model — the way Vim thinks about text.&lt;/p&gt;
&lt;p&gt;This guide starts from zero. By the end, you&apos;ll have everything you need to start.&lt;/p&gt;
&lt;h2&gt;The Basics&lt;/h2&gt;
&lt;h3&gt;Modes&lt;/h3&gt;
&lt;p&gt;Vim has four modes: Normal, Insert, Visual, and Command-line.&lt;/p&gt;
&lt;p&gt;Other editors have one mode. You type, it appears. Simple.&lt;/p&gt;
&lt;p&gt;Vim separates &lt;em&gt;navigating&lt;/em&gt; from &lt;em&gt;editing&lt;/em&gt;. You move in Normal mode. You type in Insert mode. You select in Visual mode. You run commands in Command-line mode.&lt;/p&gt;
&lt;p&gt;This sounds complicated. It isn&apos;t. You&apos;ll switch between modes constantly, and it&apos;ll become muscle memory.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../assets/blog/the-vim-guide-for-vs-code-users/wAUpUM.png&quot; alt=&quot;The four modes&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;Normal Mode&lt;/h3&gt;
&lt;p&gt;When you open Vim, you&apos;re in Normal mode. Your keyboard doesn&apos;t type — it commands.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;h/j/k/l&lt;/code&gt; move the cursor. Left, down, up, right.&lt;/p&gt;
&lt;p&gt;Why not arrow keys? Because your fingers never leave the home row.&lt;/p&gt;
&lt;p&gt;Commands take counts. Want to move down 10 lines? &lt;code&gt;10j&lt;/code&gt;. Delete 5 lines? &lt;code&gt;5dd&lt;/code&gt;. This is faster than holding down a key.&lt;/p&gt;
&lt;h3&gt;Moving Around&lt;/h3&gt;
&lt;p&gt;Here&apos;s what you need:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;h/j/k/l&lt;/code&gt; — left, down, up, right&lt;/li&gt;
&lt;li&gt;&lt;code&gt;^&lt;/code&gt; / &lt;code&gt;$&lt;/code&gt; — start / end of line&lt;/li&gt;
&lt;li&gt;&lt;code&gt;w&lt;/code&gt; / &lt;code&gt;b&lt;/code&gt; — next word / previous word&lt;/li&gt;
&lt;li&gt;&lt;code&gt;f{char}&lt;/code&gt; / &lt;code&gt;F{char}&lt;/code&gt; — jump to next/previous occurrence of a character&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;h&lt;/code&gt; and &lt;code&gt;l&lt;/code&gt; are left and right. &lt;code&gt;j&lt;/code&gt; goes down (your index finger&apos;s home position). &lt;code&gt;k&lt;/code&gt; goes up.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;^&lt;/code&gt; and &lt;code&gt;$&lt;/code&gt; work like regex anchors. Start of line, end of line.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;w&lt;/code&gt; and &lt;code&gt;b&lt;/code&gt; hop between words. &lt;code&gt;2w&lt;/code&gt; moves two words forward.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;f{char}&lt;/code&gt; is powerful. Want to jump to the next &lt;code&gt;=&lt;/code&gt;? Press &lt;code&gt;f=&lt;/code&gt;. Done.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../assets/blog/the-vim-guide-for-vs-code-users/gyKgUn.png&quot; alt=&quot;Moving with ^ $ w b&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../assets/blog/the-vim-guide-for-vs-code-users/Pddwc3.png&quot; alt=&quot;Moving with f{char}&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Master these, and you&apos;ll never reach for your mouse again.&lt;/p&gt;
&lt;h3&gt;Insert Mode&lt;/h3&gt;
&lt;p&gt;Insert mode is what other editors call &quot;normal.&quot; You type, characters appear.&lt;/p&gt;
&lt;p&gt;To enter Insert mode from Normal mode:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;i&lt;/code&gt; — insert before cursor&lt;/li&gt;
&lt;li&gt;&lt;code&gt;a&lt;/code&gt; — insert after cursor&lt;/li&gt;
&lt;li&gt;&lt;code&gt;o&lt;/code&gt; — insert new line below&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;../../assets/blog/the-vim-guide-for-vs-code-users/diLxcP.png&quot; alt=&quot;Entering Insert mode&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;Variations&lt;/h3&gt;
&lt;p&gt;Uppercase versions do more:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;I&lt;/code&gt; — insert at start of line (like &lt;code&gt;^i&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;A&lt;/code&gt; — insert at end of line (like &lt;code&gt;$a&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;O&lt;/code&gt; — insert new line above (like &lt;code&gt;ko&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To exit Insert mode: &lt;code&gt;Esc&lt;/code&gt; or &lt;code&gt;Ctrl+[&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The workflow: Normal mode to navigate, Insert mode to type, back to Normal mode. Like a painter picking up the brush, painting, putting it down to think.&lt;/p&gt;
&lt;p&gt;This feels awkward at first. Then it becomes automatic. Then regular editors feel broken.&lt;/p&gt;
&lt;h3&gt;Visual Mode&lt;/h3&gt;
&lt;p&gt;How do you delete text in Vim?&lt;/p&gt;
&lt;p&gt;You could enter Insert mode and use backspace. But that&apos;s slow.&lt;/p&gt;
&lt;p&gt;In Normal mode, &lt;code&gt;x&lt;/code&gt; deletes a character. &lt;code&gt;d&lt;/code&gt; deletes a range.&lt;/p&gt;
&lt;p&gt;Common &lt;code&gt;d&lt;/code&gt; commands:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;dd&lt;/code&gt; — delete entire line&lt;/li&gt;
&lt;li&gt;&lt;code&gt;diw&lt;/code&gt; — delete current word&lt;/li&gt;
&lt;li&gt;&lt;code&gt;dt{char}&lt;/code&gt; — delete until character&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;But what if you want to select exactly what to delete?&lt;/p&gt;
&lt;p&gt;Press &lt;code&gt;v&lt;/code&gt; to enter Visual mode. Move with &lt;code&gt;h/j/k/l/w/b/f&lt;/code&gt;. The selection follows. Press &lt;code&gt;d&lt;/code&gt; to delete.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../assets/blog/the-vim-guide-for-vs-code-users/lNuHSh.png&quot; alt=&quot;Selecting in Visual mode&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;V&lt;/code&gt; selects entire lines. &lt;code&gt;Ctrl+v&lt;/code&gt; selects columns.&lt;/p&gt;
&lt;h3&gt;Cut, Copy, Paste&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;d&lt;/code&gt; is cut, not delete. The text goes to a register.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;y&lt;/code&gt; is copy (yank). &lt;code&gt;yy&lt;/code&gt; copies a line.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;p&lt;/code&gt; is paste.&lt;/p&gt;
&lt;p&gt;Select with &lt;code&gt;v&lt;/code&gt;, copy with &lt;code&gt;y&lt;/code&gt;, move somewhere, paste with &lt;code&gt;p&lt;/code&gt;. Simple.&lt;/p&gt;
&lt;h3&gt;Text Objects&lt;/h3&gt;
&lt;p&gt;This is where Vim gets interesting.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;diw&lt;/code&gt; means &quot;delete inside word.&quot; &lt;code&gt;d&lt;/code&gt; is the command. &lt;code&gt;iw&lt;/code&gt; is a &lt;em&gt;text object&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;Text objects let you operate on logical chunks without moving the cursor first:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;iw&lt;/code&gt; — inside word&lt;/li&gt;
&lt;li&gt;&lt;code&gt;aw&lt;/code&gt; — around word (includes trailing space)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;i(&lt;/code&gt; — inside parentheses&lt;/li&gt;
&lt;li&gt;&lt;code&gt;a(&lt;/code&gt; — around parentheses (includes the parens)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Replace &lt;code&gt;(&lt;/code&gt; with any paired character: &lt;code&gt;i[&lt;/code&gt;, &lt;code&gt;i{&lt;/code&gt;, &lt;code&gt;i&quot;&lt;/code&gt;, &lt;code&gt;i&apos;&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;yiw&lt;/code&gt; copies the word under your cursor. &lt;code&gt;ci&quot;&lt;/code&gt; changes everything inside quotes. &lt;code&gt;da(&lt;/code&gt; deletes a function call&apos;s arguments, parentheses included.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../assets/blog/the-vim-guide-for-vs-code-users/593bq4.png&quot; alt=&quot;Copying a text object&quot; /&gt;&lt;/p&gt;
&lt;p&gt;This is Vim&apos;s superpower. Learn text objects.&lt;/p&gt;
&lt;h3&gt;Command Mode&lt;/h3&gt;
&lt;p&gt;Press &lt;code&gt;:&lt;/code&gt; in Normal mode to enter Command mode.&lt;/p&gt;
&lt;p&gt;Essential commands:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;:q!&lt;/code&gt; — quit without saving&lt;/li&gt;
&lt;li&gt;&lt;code&gt;:w&lt;/code&gt; — save&lt;/li&gt;
&lt;li&gt;&lt;code&gt;:wa&lt;/code&gt; — save all&lt;/li&gt;
&lt;li&gt;&lt;code&gt;:s/old/new/g&lt;/code&gt; — replace text&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;There are &lt;a href=&quot;http://vimdoc.sourceforge.net/htmldoc/vimindex.html#ex-cmd-index&quot;&gt;hundreds of commands&lt;/a&gt;. You&apos;ll use maybe ten.&lt;/p&gt;
&lt;h3&gt;Key Mappings&lt;/h3&gt;
&lt;p&gt;Vim lets you remap anything.&lt;/p&gt;
&lt;p&gt;In &lt;code&gt;~/.vimrc&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;nnoremap &amp;lt;C-f&amp;gt; :s/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now &lt;code&gt;Ctrl+f&lt;/code&gt; starts a find-and-replace.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;nnoremap&lt;/code&gt; means &quot;normal mode, non-recursive mapping.&quot; Use &lt;code&gt;inoremap&lt;/code&gt; for Insert mode, &lt;code&gt;vnoremap&lt;/code&gt; for Visual mode.&lt;/p&gt;
&lt;p&gt;Non-recursive means the mapping won&apos;t chain. If you map &lt;code&gt;a&lt;/code&gt; to &lt;code&gt;b&lt;/code&gt; and &lt;code&gt;c&lt;/code&gt; to &lt;code&gt;a&lt;/code&gt;, pressing &lt;code&gt;c&lt;/code&gt; gives you &lt;code&gt;a&lt;/code&gt;, not &lt;code&gt;b&lt;/code&gt;. This prevents infinite loops.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;Leader&lt;/code&gt; key is Vim&apos;s modifier. Default is &lt;code&gt;,&lt;/code&gt;. I use Space:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let g:mapleader = &quot;\&amp;lt;Space&amp;gt;&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;nnoremap &amp;lt;Leader&amp;gt;j 2j
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Press Space, then &lt;code&gt;j&lt;/code&gt;, to move down two lines.&lt;/p&gt;
&lt;p&gt;This is why I love Vim. Total control over keybindings. No conflicts. No restrictions.&lt;/p&gt;
&lt;h3&gt;Macros&lt;/h3&gt;
&lt;p&gt;Macros record and replay actions.&lt;/p&gt;
&lt;p&gt;Press &lt;code&gt;qq&lt;/code&gt; to start recording into register &lt;code&gt;q&lt;/code&gt;. Do something. Press &lt;code&gt;q&lt;/code&gt; to stop.&lt;/p&gt;
&lt;p&gt;Press &lt;code&gt;@q&lt;/code&gt; to replay. &lt;code&gt;10@q&lt;/code&gt; replays ten times.&lt;/p&gt;
&lt;p&gt;Example: add prefix and suffix to 10 lines.&lt;/p&gt;
&lt;p&gt;Record once: &lt;code&gt;I&lt;/code&gt;, type prefix, &lt;code&gt;Esc&lt;/code&gt;, &lt;code&gt;A&lt;/code&gt;, type suffix, &lt;code&gt;Esc&lt;/code&gt;, &lt;code&gt;j&lt;/code&gt;. Stop recording.&lt;/p&gt;
&lt;p&gt;Then &lt;code&gt;9@q&lt;/code&gt; for the remaining lines.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../assets/blog/the-vim-guide-for-vs-code-users/89gI2Y.gif&quot; alt=&quot;Recording a macro&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../assets/blog/the-vim-guide-for-vs-code-users/qwpvT1.gif&quot; alt=&quot;Replaying a macro&quot; /&gt;&lt;/p&gt;
&lt;p&gt;I use macros constantly for transforming JSON, converting enums, reformatting data.&lt;/p&gt;
&lt;h2&gt;Basic Configuration&lt;/h2&gt;
&lt;p&gt;Vanilla Vim is spartan. Add this to &lt;code&gt;~/.vimrc&lt;/code&gt;:&lt;/p&gt;
&lt;h3&gt;Line Numbers&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;set number
set relativenumber
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;number&lt;/code&gt; shows the current line. &lt;code&gt;relativenumber&lt;/code&gt; shows relative distances — essential for commands like &lt;code&gt;10j&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../assets/blog/the-vim-guide-for-vs-code-users/TmK9Ua.png&quot; alt=&quot;Relative line numbers&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;Better Indentation&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;nnoremap &amp;lt; &amp;lt;&amp;lt;
nnoremap &amp;gt; &amp;gt;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now &lt;code&gt;&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;&lt;/code&gt; indent without pressing twice.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;set shiftwidth=2
set tabstop=2
set autoindent
set smarttab
set expandtab
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Two-space indentation with spaces, not tabs.&lt;/p&gt;
&lt;h3&gt;Better Movement&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;noremap H ^
noremap L $
noremap J G
noremap K gg
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now &lt;code&gt;H/L&lt;/code&gt; go to line start/end. &lt;code&gt;J/K&lt;/code&gt; go to file start/end. Logical extensions of &lt;code&gt;h/j/k/l&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Vim in VS Code&lt;/h2&gt;
&lt;p&gt;You don&apos;t need to switch editors. VS Code has excellent Vim support.&lt;/p&gt;
&lt;p&gt;Two options:&lt;/p&gt;
&lt;h3&gt;VSCodeVim&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://marketplace.visualstudio.com/items?itemName=vscodevim.vim&quot;&gt;VSCodeVim&lt;/a&gt; emulates Vim in VS Code.&lt;/p&gt;
&lt;p&gt;Configuration lives in &lt;code&gt;settings.json&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;vim.insertModeKeyBindings&quot;: [
    { &quot;before&quot;: [&quot;j&quot;, &quot;j&quot;], &quot;after&quot;: [&quot;&amp;lt;Esc&amp;gt;&quot;] }
  ],
  &quot;vim.normalModeKeyBindingsNonRecursive&quot;: [
    { &quot;before&quot;: [&quot;H&quot;], &quot;after&quot;: [&quot;^&quot;] },
    { &quot;before&quot;: [&quot;L&quot;], &quot;after&quot;: [&quot;$&quot;] }
  ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It works. But you can&apos;t reuse your &lt;code&gt;.vimrc&lt;/code&gt;, and configuration is verbose.&lt;/p&gt;
&lt;p&gt;Good for beginners who want to stay in VS Code.&lt;/p&gt;
&lt;h3&gt;vscode-neovim&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://marketplace.visualstudio.com/items?itemName=asvetliakov.vscode-neovim&quot;&gt;vscode-neovim&lt;/a&gt; embeds actual Neovim into VS Code.&lt;/p&gt;
&lt;p&gt;This is what I use. It reads your existing Vim config. It&apos;s fast. You get real Vim, plus VS Code&apos;s extensions.&lt;/p&gt;
&lt;p&gt;The catch: you need Neovim 0.5+ installed. More setup, but worth it.&lt;/p&gt;
&lt;p&gt;Wrap VS Code-incompatible plugins in your config:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if !exists(&apos;g:vscode&apos;)
  &quot; Vim-only plugins here
endif
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Keep Going&lt;/h2&gt;
&lt;p&gt;This guide covers the fundamentals. There&apos;s much more: splits, buffers, registers, advanced motions.&lt;/p&gt;
&lt;p&gt;Read &lt;a href=&quot;https://github.com/iggredible/Learn-Vim&quot;&gt;Learn Vim the Smart Way&lt;/a&gt; or &lt;a href=&quot;https://pragprog.com/titles/dnvim2/practical-vim-second-edition/&quot;&gt;Practical Vim&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The learning curve is real. But once Vim clicks, you&apos;ll wonder how you ever edited text any other way.&lt;/p&gt;
&lt;p&gt;Start small. Use Vim mode in VS Code. Let muscle memory build. One day you&apos;ll realize the arrow keys feel wrong.&lt;/p&gt;
&lt;p&gt;That&apos;s when you know it worked.&lt;/p&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://pragprog.com/titles/dnvim2/practical-vim-second-edition/&quot;&gt;Practical Vim&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/iggredible/Learn-Vim&quot;&gt;Learn Vim the Smart Way&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;http://vimawesome.com/&quot;&gt;Vim Awesome&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;http://vimdoc.sourceforge.net/htmldoc/vimindex.html&quot;&gt;Vim documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://learnvimscriptthehardway.stevelosh.com/&quot;&gt;Learn Vimscript the Hard Way&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Native macOS Updates in Tauri</title><link>https://yuexunj.com/native-macos-updates-in-tauri/</link><guid isPermaLink="true">https://yuexunj.com/native-macos-updates-in-tauri/</guid><description>Tauri&apos;s updater gives you pipes. You build the bathroom.</description><pubDate>Sun, 18 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Four Tauri projects. Four times I wrote the same update dialog.&lt;/p&gt;
&lt;p&gt;Download progress bar. Markdown release notes. &quot;Remind Me Later&quot; button. Same code, copied across repos, maintained separately. By the fourth project, I was done.&lt;/p&gt;
&lt;h2&gt;The Problem&lt;/h2&gt;
&lt;p&gt;Tauri&apos;s built-in updater downloads binaries and verifies signatures. It has a basic dialog: release notes, two buttons, that&apos;s it.&lt;/p&gt;
&lt;p&gt;No download progress. No &quot;Remind Me Later.&quot; No background checking.&lt;/p&gt;
&lt;p&gt;You want any of that, you build it yourself.&lt;/p&gt;
&lt;p&gt;Meanwhile, every native macOS app uses Sparkle. Raycast. CleanShot. Countless others. Same dialog. Same flow. Users recognize it immediately.&lt;/p&gt;
&lt;p&gt;Sparkle handles everything I kept rebuilding: background checks, phased rollouts, localization. All the edge cases I&apos;d never think to handle.&lt;/p&gt;
&lt;p&gt;So why was I still writing custom update UI?&lt;/p&gt;
&lt;h2&gt;The Answer&lt;/h2&gt;
&lt;p&gt;Sparkle is Objective-C. Tauri is Rust. Bridging them isn&apos;t trivial.&lt;/p&gt;
&lt;p&gt;But &quot;not trivial&quot; isn&apos;t &quot;impossible.&quot;&lt;/p&gt;
&lt;h2&gt;The Plugin&lt;/h2&gt;
&lt;p&gt;So I built one. &lt;a href=&quot;https://github.com/ahonn/tauri-plugin-sparkle-updater&quot;&gt;tauri-plugin-sparkle-updater&lt;/a&gt; wraps Sparkle&apos;s &lt;code&gt;SPUStandardUpdaterController&lt;/code&gt; and exposes it to Tauri.&lt;/p&gt;
&lt;p&gt;Rust:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;tauri::Builder::default()
    .plugin(tauri_plugin_sparkle_updater::init())
    .run(tauri::generate_context!())
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;TypeScript:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { checkForUpdates } from &apos;tauri-plugin-sparkle-updater-api&apos;;

await checkForUpdates();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Call &lt;code&gt;checkForUpdates()&lt;/code&gt;. Sparkle takes over. Native dialog. Progress bar. Release notes. Done.&lt;/p&gt;
&lt;p&gt;For programmatic control, the plugin exposes 41 commands and 18 events. Build a custom flow if you want. But you don&apos;t have to.&lt;/p&gt;
&lt;h2&gt;Cross-Platform&lt;/h2&gt;
&lt;p&gt;This only works on macOS. That&apos;s the point.&lt;/p&gt;
&lt;p&gt;Cross-platform doesn&apos;t mean identical UI everywhere. It means doing each platform well.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#[cfg(target_os = &quot;macos&quot;)]
builder = builder.plugin(tauri_plugin_sparkle_updater::init());

#[cfg(not(target_os = &quot;macos&quot;))]
builder = builder.plugin(tauri_plugin_updater::Builder::new().build());
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Your Windows users get updates. Your macOS users get &lt;em&gt;native&lt;/em&gt; updates.&lt;/p&gt;
&lt;p&gt;The best update experience is invisible. Users click a button, see a familiar dialog, move on. No one should have to build that from scratch.&lt;/p&gt;
</content:encoded></item></channel></rss>