https://github.com/ShipItAndPray/pretext-editor
Canvas-rendered text editor using Pretext for line measurement. No contenteditable. Handles 1M+ lines.
https://github.com/ShipItAndPray/pretext-editor
pretext text-layout typescript typography
Last synced: about 1 month ago
JSON representation
Canvas-rendered text editor using Pretext for line measurement. No contenteditable. Handles 1M+ lines.
- Host: GitHub
- URL: https://github.com/ShipItAndPray/pretext-editor
- Owner: ShipItAndPray
- Created: 2026-03-30T15:11:26.000Z (3 months ago)
- Default Branch: master
- Last Pushed: 2026-04-02T06:16:52.000Z (2 months ago)
- Last Synced: 2026-04-04T17:14:25.643Z (2 months ago)
- Topics: pretext, text-layout, typescript, typography
- Language: TypeScript
- Homepage: https://shipitandpray.github.io/pretext-editor/
- Size: 62.5 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
- awesome-pretext - `pretext-editor` - editor/) | (Flagship Ecosystem Packages)
README
# @shipitandpray/pretext-editor
[](https://shipitandpray.github.io/pretext-editor/) [](https://github.com/ShipItAndPray/pretext-editor)
> **[View Live Demo](https://shipitandpray.github.io/pretext-editor/)**
Canvas text editor powered by [Pretext](https://github.com/chenglou/pretext) — no `contenteditable`, no DOM text nodes, handles millions of lines.
## Why
Every web text editor (Monaco, CodeMirror, Ace) uses DOM nodes for text rendering:
- Layout reflow on every keystroke
- Sluggish with 100K+ lines
- `contenteditable` is a nightmare of browser inconsistencies
This editor renders **entirely on Canvas** using Pretext for sub-pixel-accurate layout. A hidden `` captures keyboard/IME input (the same technique Monaco uses). The result: constant-time scroll, no reflow, and a clean architecture.
## Install
```bash
npm install @shipitandpray/pretext-editor @chenglou/pretext
```
## Usage (Vanilla)
```ts
import { Editor } from '@shipitandpray/pretext-editor'
const container = document.getElementById('editor')!
const editor = new Editor(container, { width: 800, height: 600 })
editor.setValue('Hello, world!\nLine 2')
editor.setOnChange((value) => console.log('Changed:', value))
```
## Usage (React)
```tsx
import { PretextEditor } from '@shipitandpray/pretext-editor/react'
function App() {
const [code, setCode] = useState('// type here...')
return (
)
}
```
## Theme Customization
Pass a partial theme to override colors:
```ts
const editor = new Editor(container, {
width: 800,
height: 600,
theme: {
background: '#282c34',
foreground: '#abb2bf',
cursor: '#528bff',
selection: 'rgba(82, 139, 255, 0.3)',
lineNumber: '#636d83',
gutterBackground: '#282c34',
currentLine: 'rgba(255, 255, 255, 0.03)',
},
})
```
### Default Theme (Dark)
| Property | Default |
| ------------------ | ----------------------------- |
| `background` | `#1e1e1e` |
| `foreground` | `#d4d4d4` |
| `selection` | `rgba(38, 79, 120, 0.6)` |
| `cursor` | `#aeafad` |
| `lineNumber` | `#858585` |
| `gutterBackground` | `#1e1e1e` |
| `currentLine` | `rgba(255, 255, 255, 0.04)` |
## Options
| Option | Type | Default | Description |
| ------------- | --------- | ------- | ------------------------------- |
| `width` | `number` | — | Canvas width in pixels |
| `height` | `number` | — | Canvas height in pixels |
| `font` | `string` | `14px "JetBrains Mono", ...` | CSS font string |
| `lineHeight` | `number` | `20` | Line height in pixels |
| `readOnly` | `boolean` | `false` | Disable editing |
| `wordWrap` | `boolean` | `false` | Enable soft word wrap |
| `lineNumbers` | `boolean` | `true` | Show line number gutter |
| `theme` | `object` | Dark | Partial theme overrides |
## Performance
| Metric | Target |
| ------------------------- | ------------ |
| Keystroke to render | < 2ms |
| Scroll FPS (1M lines) | 120fps |
| Initial load (100K lines) | < 200ms |
| Memory (1M lines) | < 100MB |
| Cache hit rate | > 99% |
The virtualizer only measures and renders visible lines plus a small overscan buffer. Line measurements are cached and only invalidated on edit. Cumulative heights use binary search for O(log n) scroll-to-line mapping.
## Architecture
```
Canvas (rendering) <-- Virtualizer (visible range) <-- LineLayout (Pretext measurement)
^ ^
| |
Hidden --> KeyboardHandler --> Document (line model + undo/redo)
Mouse events --> MouseHandler --> Cursor (position + selection)
```
- **Document**: Array of lines, insert/delete with full undo/redo stack
- **LineLayout**: Pretext-based line measurement, wrapping, cumulative heights
- **Virtualizer**: Determines visible range with overscan, ensures cursor visibility
- **CanvasRenderer**: Draws text, gutter, selections, cursor via Canvas 2D
- **KeyboardHandler**: Hidden textarea for input capture (handles IME, autocorrect)
- **MouseHandler**: Click-to-position, drag-to-select via hitTest
## License
MIT