Hard Wrap
pendingby alethiophile
Transparently hard-wrap markdown files on disk while displaying them as soft-wrapped paragraphs in the editor.
Hard Line Wrap for Obsidian
Transparently hard-wrap markdown files on disk while displaying them as soft-wrapped paragraphs in the editor.
Files are always stored with hard line breaks at a configurable column width (default 80), making them pleasant to read in a terminal, emacs, git diffs, etc. Obsidian's editor sees the unwrapped form — one long line per paragraph — and handles visual wrapping natively.
Configuration
Global settings
In Settings > Hard Wrap:
| Setting | Default | Description |
|---|---|---|
| Enable hard-wrapping | on | Master toggle |
| Column width | 80 | Wrap target (40–200) |
Per-file override
Add a hard-wrap key to a file's frontmatter:
---
hard-wrap: 60 # wrap at 60 columns
---
---
hard-wrap: false # disable for this file
---
---
hard-wrap: true # enable with global default columns
---
Omitting the key uses the global default.
Commands
- Reformat current file with hard wraps — normalizes the editor content by running unwrap → wrap → unwrap. This cleans up hard breaks that the plugin didn't insert — e.g. text pasted from an external source, or soft wraps added manually — without requiring a save/reload cycle. Also useful after changing the column width or after external edits.
How it works
The plugin monkey-patches three methods on MarkdownView.prototype
using monkey-around:
- On load (
setViewData): hard-wrapped paragraphs are joined into single lines before the content reaches the editor. - On save (
getViewData): the editor's single-line paragraphs are re-wrapped at the configured column width before being written to disk. - On mode switch (
setMode): when switching back to source mode,this.datais re-unwrapped. Obsidian'ssetModebypassessetViewDataand feedsthis.datadirectly to the editor, so without this patch the editor would show hard-wrapped lines after toggling between source and reading mode.
Both auto-save (the debounced requestSave) and manual save (Ctrl+S, close) go
through getViewData, so all save paths produce hard-wrapped output. Patching
getViewData rather than save is important because Obsidian also calls
getViewData outside the save path. When a file is modified externally while
the editor has unsaved changes, Obsidian performs a 3-way merge between
lastSavedData, the current getViewData() return value, and the new disk
content. Since lastSavedData and the disk content are both in hard-wrapped
form, getViewData() must return wrapped text too — otherwise the merge would
treat every re-wrapped line break as a conflicting change. Obsidian also uses
getViewData() for change detection (comparing against lastSavedData to
decide whether a save is needed), which likewise requires consistent
representations.
On plugin unload, all open markdown files are force-saved so they remain hard-wrapped on disk.
What gets wrapped
The transform follows CommonMark soft-line-break semantics. Anything a markdown renderer would join into a single paragraph gets wrapped/unwrapped:
- Plain paragraphs
- Blockquote content (including nested blockquotes)
- List item content (unordered, ordered, nested)
- Footnote definitions
- Callout body text
Everything else passes through verbatim:
- Frontmatter (YAML between
---fences) - Fenced code blocks (backtick and tilde)
- Indented code blocks
- ATX and setext headings
- Tables
- Thematic breaks
- HTML blocks
- Callout headers (
[!type]lines)
Within wrappable text, inline elements are never broken across lines:
- Links:
[text](url)and[text][ref] - Images:
 - Inline code:
`code`
Hard line breaks (trailing \ or two trailing spaces) are preserved.
Build
Requires Node.js >= 18.
npm install
npm run build # production build → main.js
npm run dev # watch mode (rebuilds on change)
npm test # run unit tests
The build uses esbuild. Output is a single main.js (CommonJS). The
obsidian package and @codemirror/* are externals (provided by
Obsidian at runtime). monkey-around is bundled.
Known limitations
- Search: Obsidian indexes on-disk (hard-wrapped) content. A search for a phrase that spans a wrap boundary won't match.
Note on code authorship
This plugin and its documentation were substantially created using AI. They have been tested and reviewed by the human author.
For plugin developers
Search results and similarity scores are powered by semantic analysis of your plugin's README. If your plugin isn't appearing for searches you'd expect, try updating your README to clearly describe your plugin's purpose, features, and use cases.