Files
typst-leaf/PLAN.md
T
nav.sikand bfb4d8bef8 feat: Git workflow, Vim mode, and preview→editor SyncTeX
Add three core VISION features on top of the barebones MVP:

Git workflow (backend + frontend):
- Backend: simple-git wrapper with per-project repo isolation, init
  (idempotent .gitignore reconciliation, handles empty repos), status,
  log, commit (per-commit identity via -c flags), remote, push, pull
- gitFor() now refuses to run without the project's own .git, preventing
  accidental parent-repo detection when projects/ lives inside the
  typst-leaf worktree
- Frontend: GitPanel (branch, ahead/behind, inline commit, push/pull,
  remote prompt, collapsible log), SettingsPanel (git identity in
  localStorage), Ctrl+Shift+S focuses commit input
- Status/log refresh driven by gitVersion bumps (no polling) with
  cancellation guards across project switches
- Auto-init on status failure for pre-existing projects

Vim mode:
- Off-by-default toggle persisted to localStorage
- Dynamic monaco-vim import, status bar div, StrictMode-safe lifecycle
  via editorReady reset on unmount

SyncTeX (preview → editor):
- Handle tinymist/preview/scrollSource notification and
  window/showDocument request
- Cross-platform path normalization (Windows drive letters, native
  backslash paths, URL-decoded paths)
- Editor jump effect with Monaco selection/reveal/focus, clears on
  completion

Robustness fixes across review cycles:
- Cancellation guards on git status/log effects to prevent stale writes
- bumpGitVersion after auto-init so log viewer updates
- jsonOrThrow extracts structured backend errors, handles empty bodies
- SettingsPanel/GitPanel validate localStorage identity shape
- GitPanel clears log state on project switch
- File path URI encoding preserves path separators (segment-wise encode)
- tsconfig lib bumped to ES2021 for String.replaceAll

AGENTS.md: removed stale execa/monaco-languageclient references

PLAN.md: implementation plan for this chunk
2026-07-02 00:35:32 +05:30

3.9 KiB

Next Chunk: Git + Vim + SyncTeX (Core VISION)

Part A — Vim Mode (frontend-only)

Goal: Off-by-default vim toggle, persisted to localStorage.

  • Editor.tsx:
    • Always render <div ref={vimStatusRef} /> (empty when vim is off, monaco-vim populates it when on).
    • On handleMount: if localStorage.getItem("typst-leaf.vim") === "true", initVimMode(editor, vimStatusRef.current).
    • Add "Vim" toggle button in the editor toolbar (emerald when active).
    • On toggle: persist to localStorage; init or dispose accordingly.
    • Store initVimMode's returned controller in a ref for disposal on unmount/toggle-off.

Part B — Git

Backend

  • git.ts: factory gitFor(project: string)simpleGit(projectDir(project)).
  • routes/git.ts (mounted at /api/projects/:project/git):
    • POST /initgit init, set default identity from env, initial commit, .gitignore.
    • GET /status — branch, ahead/behind, staged/unstaged, clean.
    • GET /log?limit=20 — commit history.
    • POST /commit — body { message, all?, identity? }. Uses -c user.name=... -c user.email=... per-commit flags (not repo config).
    • PUT /remote — body { url }git remote add or set-url.
    • POST /push / POST /pull — to origin, return { ok, error? }.
  • routes/projects.ts: POST /:project calls git.init after seeding main.typ.
  • index.ts: mount gitRouter.

Frontend

  • api.ts: gitInit, gitStatus, gitLog, gitCommit, gitPush, gitPull, setRemote.
  • types.ts: FileStatus, CommitInfo, GitStatus.
  • GitPanel.tsx (sidebar, below FileTree):
    • Branch name, ahead/behind badges, unstaged file count.
    • Inline commit: message input + Commit button (Enter to submit).
    • Push/Pull buttons; prompts for remote URL if none configured.
    • Refreshes on project open + after saves + after commits.
  • SettingsPanel.tsx (gear icon in sidebar header toggles panel):
    • Git Author Name, Git Author Email inputs.
    • Persisted to localStorage["typst-leaf.git-identity"].
    • Sent with every commit as identity.
  • Editor.tsx: CtrlCmd+Shift+KeyS → dispatch commitFocus via app state.
  • app-state.tsx: gitStatus, gitVersion (incremented on save to trigger refresh), commitFocus (incremented on shortcut).

Part C — SyncTeX (Preview → Editor)

LSP Client (lsp.ts)

Two LSP message paths from tinymist, handle both:

  1. tinymist/preview/scrollSource notification:

    • Params: { filepath: string, start?: [number, number], end?: [number, number] }
    • Convert: filepath → relPath, start/end are 0-indexed [row, col]
    • Call onJumpToSource({ filepath, start, end })
  2. window/showDocument request:

    • Params: { uri: string, selection?: { start: {line, character}, end: {line, character} } }
    • Convert: uri (file://) → relPath, selection is 0-indexed LspRange
    • Call onJumpToSource(...), respond { success: true }

App State (app-state.tsx)

  • Add jumpTarget: { file: string; start?: [number, number]; end?: [number, number] } | null to state.
  • In startLsp, pass onJumpToSource callback:
    1. Convert absolute filepath → relPath (strip new URL(projectUri).pathname prefix)
    2. Ignore if filepath is not under the project (e.g. imported package file)
    3. If relPath !== state.openFile, dispatch openFile
    4. Dispatch setJumpTarget({ file: relPath, start, end })

Editor (Editor.tsx)

  • Effect deps: [jumpTarget, model]
  • When jumpTarget is truthy AND model is the correct file:
    • Convert 0-indexed [row, col] → Monaco 1-indexed (line = row + 1, column = col + 1)
    • editor.setSelection(range), editor.revealRangeInCenter(range), focus editor
    • Dispatch clearJumpTarget

Part D — Doc Cleanup

  • AGENTS.md: Remove stale execa (backend has simple-git, not execa). Update Architecture & Boundaries to reflect that frontend uses raw LSP, not monaco-languageclient. Fix stale vscode-ws-jsonrpc references (frontend removed it; backend still uses it).