npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

pretext-editor

v0.6.14

Published

High-performance Canvas-virtualized text editor for large documents based @chenglou/pretext

Readme

pretext-editor

A lightweight, high-performance Canvas-virtualized code editor with VS Code-style keyboard shortcuts, syntax highlighting, and multi-cursor editing.

Built on @chenglou/pretext + vscode-textmate. Integrates with React / Vue 3 / Svelte / Angular.

Features

  • Canvas virtual scrolling — fluid editing of 10,000+ line files; only visible lines are rendered
  • Syntax highlighting — vscode-textmate + Oniguruma, 30+ languages
  • VS Code shortcuts — navigation, editing, selection, clipboard, history
  • Multi-cursor editing — Alt+Click, Ctrl+D (next occurrence), Ctrl+Shift+L (all occurrences)
  • Column selection — Alt+Shift+drag
  • Indent guides — auto-detected indent unit with active-scope bracket highlighting
  • Undo / redo — 200-entry snapshot stack

Install

npm install pretext-editor

Framework Support

| Framework | Extra setup needed | |-----------|-------------------| | React | None — import and use | | Vue 3 | None — import and use | | Svelte | None — import and use | | Angular | Copy editor.component.ts + create Worker | | Vanilla / no framework | Use EditorController directly |

React, Vue, and Svelte all require one line in your Vite config (see below). Angular does not use Vite.

Vite Setup (React / Vue / Svelte)

Add one line to your vite.config.ts:

export default defineConfig({
  optimizeDeps: { exclude: ['pretext-editor'] },
})

This prevents Vite from pre-bundling the package with esbuild, which would break the syntax-highlighting worker.


React

import { PretextEditor } from 'pretext-editor/react'
import 'pretext-editor/react/index.css'

function App() {
  return (
    <div style={{ height: '100vh' }}>
      <PretextEditor value="console.log('hello')" language="typescript" />
    </div>
  )
}

The component is uncontrolled for typing — you pass value to set initial content or to replace it externally (e.g. loading a file), but you do not need to sync state on every keystroke.

To react to edits, use onTextChanged:

<PretextEditor
  value={code}
  language="typescript"
  onTextChanged={(r1, c1, r2, c2, oldValue, newValue) => {
    setCode(newValue)
  }}
/>

To get a handle for scrolling:

const ref = useRef<PretextEditorHandle>(null)
<PretextEditor ref={ref} value={code} language="typescript" />
ref.current?.scrollTextTo(0, 42)

Vue 3

<template>
  <div style="height: 100vh">
    <PretextEditor
      :value="code"
      @text-changed="code = $event.newValue"
      language="typescript"
    />
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { PretextEditor } from 'pretext-editor/vue'
import 'pretext-editor/vue/index.css'

const code = ref("console.log('hello')")
</script>

The component emits @text-changed with { r1, c1, r2, c2, oldValue, newValue } on every edit, and @cursor-updated with { line, col } on cursor moves.


Svelte

<script lang="ts">
  import PretextEditor from 'pretext-editor/svelte'

  let code = "console.log('hello')"

  function handleTextChanged(e: CustomEvent<{ r1: number; c1: number; r2: number; c2: number; oldValue: string; newValue: string }>) {
    code = e.detail.newValue
  }
</script>

<div style="height: 100vh">
  <PretextEditor value={code} language="typescript" on:text-changed={handleTextChanged} />
</div>

The component dispatches a text-changed CustomEvent with { r1, c1, r2, c2, oldValue, newValue } on every edit, and cursor-updated with { line, col } on cursor moves. Use bind:this to get a handle reference.


Angular

Angular requires two extra steps: copy the component into your project, and create the highlight worker yourself.

Step 1 — Copy the component file:

cp node_modules/pretext-editor/dist/angular/editor.component.ts src/app/pretext-editor/editor.component.ts

Step 2 — Use it in your component, passing a Worker via [worker]:

import { Component } from '@angular/core'
import { PretextEditorComponent } from './pretext-editor/editor.component'

@Component({
  standalone: true,
  imports: [PretextEditorComponent],
  template: `
    <pretext-editor
      [value]="code"
      [worker]="worker"
      language="typescript"
      (textChanged)="code = $event.newValue"
      style="height: 100vh; display: block"
    />
  `,
})
export class AppComponent {
  code = "console.log('hello')"

  readonly worker = typeof Worker !== 'undefined'
    ? new Worker(
        new URL(
          // Adjust the relative path to match your component file's location
          '../../node_modules/pretext-editor/dist/highlight.worker.bundle.js',
          import.meta.url,
        ),
        { type: 'module' },
      )
    : undefined
}

Create the Worker at class level (not inside a lifecycle hook) so WASM loading starts before the editor mounts.


Vanilla / No Framework

Use EditorController directly:

import { EditorController } from 'pretext-editor'
import { createWorker } from 'pretext-editor/worker-create'

const container = document.querySelector('.editor-scroll')
const canvas    = document.querySelector('.editor-canvas')
const textarea  = document.querySelector('.editor-textarea')
const content   = document.querySelector('.editor-content')

const ctrl = new EditorController({
  value: "console.log('hello')",
  language: 'typescript',
  worker: createWorker(),
})

ctrl.mount(container, canvas, textarea, () => {
  // called on every state change — update your own UI here
  const state = ctrl.getState()
}, content)

You are responsible for the DOM structure (.editor-scroll, .editor-canvas, .editor-textarea, .editor-content) and CSS. See demo/vanilla/ for a full example.


Electron / electron-vite

For Electron projects, the standard optimizeDeps.exclude approach fails because Electron's renderer cannot resolve new Worker(new URL(...)) paths inside node_modules. Use pretextEditorBundlePlugin() instead — it inlines the entire highlight worker as a blob URL at build time, bypassing all path resolution.

// vite.config.ts (renderer)
import { pretextEditorBundlePlugin } from 'pretext-editor/vite'

export default defineConfig({
  plugins: [react(), pretextEditorBundlePlugin()],
  // optimizeDeps.exclude is injected automatically
})

For non-Electron Vite projects (browser only), optimizeDeps.exclude remains sufficient and keeps the bundle lighter.


Props

| Prop | React | Vue | Svelte | Angular | Type | Default | |------|-------|-----|--------|---------|------|---------| | value | ✓ | ✓ | ✓ | ✓ | string | '' | | language | ✓ | ✓ | ✓ | ✓ | string | — | | theme | ✓ | ✓ | ✓ | ✓ | string | 'dark-plus' | | fontSize | ✓ | ✓ | ✓ | ✓ | number | 14 | | fontFamily | ✓ | ✓ | ✓ | ✓ | string | Menlo, Monaco, … | | tabSize | ✓ | ✓ | ✓ | ✓ | number | 4 | | wordWrap | ✓ | ✓ | ✓ | ✓ | boolean | false | | keymap | ✓ | ✓ | ✓ | ✓ | Partial<Record<CommandId, KeyBinding>> | — | | worker | — | — | — | ✓ | Worker | — | | components | ✓ | — | — | — | { ContextMenu?, SearchBar? } | — |

Text change callbacks — fired with full diff (r1, c1, r2, c2, oldValue, newValue):

| Framework | Callback | |-----------|----------| | React | onTextChanged?: (r1, c1, r2, c2, oldValue, newValue) => void | | Vue | @text-changed="({ r1, c1, r2, c2, oldValue, newValue }) => ..." | | Svelte | on:text-changedCustomEvent<{ r1, c1, r2, c2, oldValue, newValue }> | | Angular | (textChanged)="handler($event)" |

Cursor callbacks — fired with { line, col } on cursor move:

| Framework | Callback | |-----------|----------| | React | onCursorUpdated?: ({ line, col }) => void | | Vue | @cursor-updated="({ line, col }) => ..." | | Svelte | on:cursor-updatedCustomEvent<{ line, col }> | | Angular | (cursorUpdated)="handler($event)" |

Scroll callbacks — deduplicated, fire independently:

| Framework | onScroll | onTextScroll | |-----------|----------|--------------| | React | onScroll?: (x, y, fromUser) => void | onTextScroll?: (col, line, fromUser) => void | | Vue | @scroll="({ x, y, fromUser }) => ..." | @text-scroll="({ col, line, fromUser }) => ..." | | Svelte | on:scrollCustomEvent<{ x, y, fromUser }> | on:text-scrollCustomEvent<{ col, line, fromUser }> | | Angular | (scroll)="handler($event)"$event: { x, y, fromUser } | (textScroll)="handler($event)"$event: { col, line, fromUser } |

fromUser is true when scrolled by the user, false when triggered by scrollTo / scrollTextTo.

Custom Context Menu & Search Bar

React — pass component references via components:

<PretextEditor
  components={{
    ContextMenu: ({ builtins, onClose }) => (
      <MyMenu items={[builtins.copy, builtins.paste]} onClose={onClose} />
    ),
    SearchBar: ({ state, actions }) => (
      <MySearchBar state={state} actions={actions} />
    ),
  }}
/>

Vue — use scoped slots:

<PretextEditor>
  <template #context-menu="{ builtins, onClose }">
    <MyMenu :builtins="builtins" :on-close="onClose" />
  </template>
  <template #search-bar="{ state, actions }">
    <MySearchBar :state="state" :actions="actions" />
  </template>
</PretextEditor>

Svelte — use named slots:

<PretextEditor>
  <svelte:fragment slot="context-menu" let:builtins let:onClose>
    <MyMenu {builtins} {onClose} />
  </svelte:fragment>
  <svelte:fragment slot="search-bar" let:state let:actions>
    <MySearchBar {state} {actions} />
  </svelte:fragment>
</PretextEditor>

Angular — use ng-template with #contextMenu / #searchBar:

<pretext-editor>
  <ng-template #contextMenu let-builtins="builtins" let-onClose="onClose">
    <my-menu [builtins]="builtins" (close)="onClose()" />
  </ng-template>
  <ng-template #searchBar let-state="state" let-actions="actions">
    <my-search-bar [state]="state" [actions]="actions" />
  </ng-template>
</pretext-editor>

ContextMenuBuiltins has four entries: copy, cut, paste, selectAll — each a ContextMenuItem.

SearchActions methods: setQuery · next · prev · close · setCaseSensitive · setWholeWord · setUseRegex · toggleReplace · setReplaceQuery · setPreserveCase · replace · replaceAll

Themes

Built-in values for theme: 'dark-plus' · 'dracula' · 'github-light'

Handle Methods

// React
const ref = useRef<PretextEditorHandle>(null)
ref.current?.scrollTextTo(0, 42)

// Vue
const editorRef = ref<PretextEditorHandle>()

// Svelte
let editorRef: PretextEditorHandle
<PretextEditor bind:this={editorRef} ... />

// Angular — via @ViewChild
@ViewChild(PretextEditorComponent) editor!: PretextEditorComponent
this.editor.scrollTextTo(0, 42)

| Method | Description | |--------|-------------| | getTextOffset() | Current text scroll position as { col, line } | | getVisibleLines() | { from, to } visible line range | | scrollTo(x, y) | Scroll to a pixel position | | scrollTextTo(col, line) | Scroll to a text position (col column, line number) |

Keyboard Shortcuts

| Shortcut | Action | |----------|--------| | ↑ ↓ ← → | Move cursor | | Ctrl+← → | Move by word | | Home / End | Line start / end | | Ctrl+Home / End | File start / end | | Shift+arrows | Extend selection | | Ctrl+A | Select all | | Ctrl+L | Select current line | | Ctrl+D | Select next occurrence | | Ctrl+Shift+L | Select all occurrences | | Alt+Click | Add / remove cursor | | Alt+Shift+drag | Column selection | | Ctrl+Backspace / Delete | Delete by word | | Tab / Shift+Tab | Indent / dedent | | Alt+↑ ↓ | Move line up / down | | Alt+Shift+↑ ↓ | Copy line up / down | | Ctrl+Enter | Insert line below | | Ctrl+Shift+Enter | Insert line above | | Ctrl+/ | Toggle line comment | | Ctrl+Shift+K | Delete line | | Ctrl+Z / Ctrl+Y | Undo / redo | | Ctrl+F | Open search |

Supported Languages

typescript · tsx · javascript · jsx · python · rust · go · c · cpp · csharp · java · kotlin · swift · ruby · php · css · scss · less · html · xml · vue · svelte · json · jsonc · yaml · toml · markdown · bash · fish · sql · graphql · lua · dart · scala · r · haml · glsl · postcss

import { extToLang } from 'pretext-editor'
extToLang('ts')  // → 'typescript'
extToLang('py')  // → 'python'

Demos

cd demo/react    && npm install && npm run dev   # React + Vite
cd demo/vue      && npm install && npm run dev   # Vue 3 + Vite
cd demo/svelte   && npm install && npm run dev   # Svelte + Vite
cd demo/angular  && npm install && npm run dev   # Angular
cd demo/vanilla  && npm install && npm run build # Vanilla — open index.html after build

License

MIT