Repository navigation
How Code Appearance Works
This chapter covers the settings that change how code looks in every editor, diff side and merge pane: font weight, line spacing, syntax highlighting, render whitespace, the current line, indentation, zoom and ligatures. The cursor has its own chapter, How the Cursor Works. The commands and keys are in How editing code works. The user side is in Code Appearance.
People read code here all day next to their IDE, so it should look like their IDE: the same spacing, the same visible whitespace and the same indentation. A file indented with 2 spaces must not get 4 when you press Tab, and a look setting must reach the editors that are already open.
flowchart LR
S[Settings store] -->|$effect in App.svelte| F[setRenderWhitespace, setIndentation, setWordWrap]
F -->|effect or reconfigure| V[Every registered EditorView]
O[Editor opens] --> D[detectIndentation on its text]
D --> T[tabSize and indentUnit]
T --> V
V --> B[Status bar: Spaces or Tab Size]
Line spacing. settings.editorLineHeight (default 1.25, from 1.0 to 2.5) is validated by pickNumber and rounded in settingsData.ts. applyAppearance writes it to --code-line-height on the root element, and editorTheme reads it for .cm-scroller. Every editor, diff side and merge pane uses editorTheme, and Markdown code blocks read the same variable, so one CSS variable changes them all without rebuilding a view.
Font weight. settings.editorFontWeight (default 400, from 100 to 900) is validated by pickNumber and rounded to a step of 100, and fontWeightName gives the style name the slider shows (300 is Light). It works like line spacing: applyAppearance writes --code-weight, and editorTheme (.cm-scroller), the sticky scroll header and Markdown code blocks read it. .tok-heading and .tok-strong in the UI library's styles/code.css use calc(var(--code-weight) + 200), so bold text stays bolder than the code around it at every weight.
Syntax highlighting. Every editor, diff side and merge pane gets its language from editorLanguage(filePath) in languages.ts: a Compartment holding either the grammar (languageFor, a dynamic import) or, with syntaxHighlighting off, only EditorState.languageData with the file's commentTokens, so Toggle Comment still works without a parser. A facet in the compartment remembers the file and which of the two it holds. setSyntaxHighlighting, called from an effect in App.svelte, swaps the compartment in every registered view; an editor built while the setting changed catches up from its registry plugin. Dropping the grammar drops the language state field and its syntax tree. The Markdown preview's CodeHighlighter has setEnabled, which clears its parsers and cached HTML and renders again.
Render whitespace. whitespace.ts has a pure part and a view part. whitespaceRuns(text, mode) finds the stretches of spaces and the tabs one line draws for a mode (none, boundary, selection, trailing, all), and clipRuns cuts them to the selection. A ViewPlugin decorates only view.visibleRanges with mark decorations: a dotted background for spaces and a drawn arrow for tabs. The text itself never changes, so copying gives real spaces. With none the extension adds nothing at all.
The mode sits in a Compartment. A tiny ViewPlugin registers every live view in a Set, and App.svelte calls setRenderWhitespace from an effect when the setting changes, which reconfigures the compartment in each registered view. Open editors follow at once.
Indentation. indentDetect.ts is pure: detectIndentation(lines) reads up to 10,000 lines and answers tabs when more lines start with a tab, else the most common step by which the indentation grows from one line with code to the next. It skips blank lines, one-space steps (comment stars) and steps wider than 8 (alignment), and returns null when there is nothing to follow. indentation.ts keeps the answer in a StateField that is filled from the document when the editor state is created, and EditorState.tabSize and indentUnit are computed from it. App.svelte calls setIndentation when detectIndentation or tabSize changes, which sends an effect to every registered view so each reads its text again. The status bar reads editorIndent(state).
The current line. activeLine.ts replaces CodeMirror's highlightActiveLine with highlightActiveLineWhenEmpty: the cursor lines get cm-activeLine only while every selection range is empty. Read-only panes have no current line highlight. Why this matters is in the bug below.
Zoom and ligatures. Ctrl or Cmd plus the mouse wheel zooms, only over a .cm-editor: App.svelte listens and asks a WheelZoom (src/lib/editor/wheelZoom.ts) for the next size. View > Zoom In and Zoom Out call steppedFontSize from the same module through menuActions.ts. Font ligatures are a data-ligatures root attribute set by applyAppearance and styled in the UI library's styles/code.css.
| File | What it does |
|---|---|
src/lib/editor/languages.ts |
editorLanguage, setSyntaxHighlighting, the comment style of each grammar |
ui/web/src/lib/markdown/highlight.ts |
CodeHighlighter.setEnabled for Markdown code blocks |
src/lib/editor/whitespace.ts |
Render whitespace |
src/lib/editor/indentDetect.ts |
detectIndentation, pure |
src/lib/editor/indentation.ts |
The editor's tab size and indent unit, setIndentation, editorIndent
|
src/lib/editor/activeLine.ts |
The current line highlight |
src/lib/editor/setup.ts |
editorTheme and baseExtensions
|
src/lib/editor/wheelZoom.ts |
Wheel zoom steps and steppedFontSize
|
src/lib/stores/settingsData.ts |
editorFontWeight, fontWeightName, editorLineHeight, renderWhitespace, tabSize, detectIndentation and their checks |
src/lib/views/StatusBar.svelte |
Spaces or Tab Size for the file on screen |
Font weight in steps of 100. Fonts name their weights in hundreds (Light, Regular, Medium), so the slider shows a name instead of a bare number. Weights in between only work with variable fonts, so they would look the same as a step for most fonts.
Selection by default. Drawing every space is noisy, and drawing none hides trailing spaces. The default, dots only inside the selection, shows them exactly when you look.
Detect only on open. Indentation is read when an editor opens and when a setting changes, not while you type, so the indent unit never jumps under the cursor. Steps into a deeper level count, steps back out do not, because closing several blocks at once makes a step of many levels.
Syntax highlighting off means no grammar. Only hiding the colors would keep the parser and its tree, which is where the memory goes, so the switch removes the language itself.
Only visible lines. Decorating the whole document would cost memory on big files. The plugin rebuilds on scroll, edits and, in Selection mode, selection changes.
-
src/lib/editor/whitespace.test.ts: the runs of each mode and clipping to selections. -
src/lib/editor/languageName.test.ts: editors get the grammar or only the comment style, and Toggle Comment works without a grammar.src/lib/markdown/highlight.test.ts: code blocks turn plain and come back. -
src/lib/editor/activeLine.test.ts: the highlight shows only without a selection. -
src/lib/editor/indentDetect.test.ts: 2 and 4 spaces, tabs, mixed files, comment stars, alignment and the line limit. -
src/lib/stores/settingsData.test.tsandsrc/lib/editor/wheelZoom.test.ts: setting checks and zoom steps. -
src/lib/menu/menuState.test.ts: View > Detect Indentation follows the setting.
- A new look setting: follow
--code-line-height(a CSS variable) orrenderWhitespace(a compartment), and update Code Appearance, Settings and Settings Reference. - Retake
editor-whitespace.pngwhen whitespace drawing changes.
The selection hidden by the current line highlight is recorded in How the editor works.
These pages are generated from docs/wiki. To correct something, open a pull request against the source file; edits made here are overwritten.
Using Nibtree
- Getting Started
- Welcome Screen
- Workspaces
- Files Panel
- File Operations
- File Icons
- Rounded Panels
- Editor and Tabs
- Pin Reorder and Wrap Tabs
- Single Tab Title
- Split Editor
- New File and Unsaved Changes
- Unload Hidden Tabs
- Editing Code
- Code Appearance
- Markdown Editor
- Image and PDF Preview
- Search Everywhere
- Recent Files
- Navigation Bar
- Find and Replace
- Changes and Commits
- Sign In Prompts
- Commit Box Layout
- Repository Actions
- Commit Options
- Ignoring Files
- Diffs
- Inline Diffs
- Staging View
- Revert a Change
- Resolving Conflicts
- Merge Tool
- Blame
- History and Log
- Merge Commits
- Branches and Tags
- Branches Popup
- Remotes
- Stashes
- Shelf
- Git Menu
- Git Dialogs
- Interactive Rebase
- Worktrees
- AI Agents
- AI Tasks
- AI Usage
- Keep Computer Awake
- Submodules
- Git LFS
- GitHub
- Git Console
- Terminal
- Terminal Features
- Scripts
- Navigation
- Menus
- Settings
- Settings Search
- Color Themes
- Settings Terminal and Automation
- Settings Files
- Updates
- Status Bar and Help
- Notifications
- Memory Use
- Clear Cache
- MCP and CLI
- Git Mergetool
- Keyboard Shortcuts
- Keyboard Shortcuts Editor
- Troubleshooting
- Troubleshooting Git
Developers
- Developer Guide
- Architecture
- Project Layout
- Backend
- How Folder Watching Works
- Folder Watching Bugs We Fixed
- How Workspace Files Work
- How the Preview Scheme Works
- Backend Services
- How Terminal Features Work
- Terminal Bugs We Fixed
- Frontend
- How the Rich Markdown Editor Works
- Rich Markdown Bugs We Fixed
- Git Menu Links
- Menu Keys and Routing
- Frontend Modules
- Blame Bugs We Fixed
- Project Color And Title Bar
- One Diff View
- Diff Bugs We Fixed
- Merge Tool Layouts
- Merge Tool Bugs We Fixed
- Color Theme Bugs We Fixed
- How the Connected Diff Works
- Rounded Panels Bugs We Fixed
- Update Bugs We Fixed
- Status Bar Bugs We Fixed
- MCP and CLI Bugs We Fixed
- Repository Actions Bugs We Fixed
- How the Changes Tab Works
- CodeMirror Patterns
- How the Cursor Works
- How Editor Features Work
- How the Path Bar Works
- Commands and Events
- Settings Reference
- Commands Workspace and Changes
- Commands Branches and Remotes
- Commands History and Extras
- Commands App and Tools
- Testing
- Test Suites
- Debugging
- Debugging with MCP
- Measuring Setting Memory
- How Terminal Memory Is Measured
- How Markdown Diagrams Work
- Releases and CI
- Publishing the Wiki
- Versioning and Changelog
- Browser Version Packages
- Browser Release Checks
- Docker Image
- Platforms and Signing
- Windows Support
- Windows Bugs We Fixed
- Windows MCP and CLI
- Docs and Screenshots
- UI Library
- UI Library MCP
- UI Library Bugs
- UI Library Migration
- Contributing
How features work
- Welcome screen
- Workspaces and folders
- Files panel
- File operations
- File icons
- Editor and tabs
- Pin, reorder and wrap tabs
- Single tab title
- Split editor
- New File and unsaved changes
- Unload hidden tabs
- Editing code
- Code appearance
- Markdown editor
- Image and PDF preview
- Search Everywhere
- Recent Files
- Navigation Bar
- Find and replace
- Changes and commits
- Sign-in prompts
- Commit box layout
- Repository actions
- Commit options
- Ignoring files
- Diffs
- Inline diffs
- Staging view
- Revert a change
- Resolving conflicts
- Merge tool
- Blame
- History and log
- Merge commits
- Commit tabs
- Branches and tags
- Branches popup
- Remotes
- Stashes
- Shelf
- Git menu
- Git dialogs
- Interactive rebase
- Worktrees
- AI agents
- AI usage
- Keep computer awake
- AI tasks
- Submodules
- Git LFS
- GitHub account
- Git Console
- Back and Forward
- Integrated terminal
- Scripts tool window
- Menu bar
- Settings
- Search in Settings
- Color themes
- Rounded panels
- Updates and release channels
- Status bar and help links
- Notifications
- Memory readout and memory log
- Clear Cache
- MCP server and command line tool
- git mergetool mode