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

@synclineapi/editor

v4.0.3

Published

A zero-dependency, pixel-perfect, fully customisable browser-based code editor

Readme

Syncline Editor

A zero-dependency, pixel-perfect, fully customisable browser-based code editor built as a TypeScript library.

Ships as both an ES module and UMD bundle, runs entirely inside a Shadow DOM, and handles 100,000+ line files via virtual rendering.


Table of Contents


Features

| Feature | Detail | |---|---| | Zero dependencies | No external runtime libraries — one self-contained bundle | | Virtual rendering | Only visible rows exist in the DOM; handles 100,000+ line files smoothly | | Shadow DOM | Fully encapsulated styles — no CSS leakage in either direction | | Dual build | ES module + UMD bundle, full TypeScript declarations | | Syntax highlighting | TypeScript, JavaScript, CSS, JSON, Markdown — nine token classes | | Token color overrides | Override individual token colours on top of any theme without replacing it | | 6 built-in themes | VR Dark, VS Code Dark+, Monokai, Dracula, GitHub Light, Solarized Light | | Custom themes | Full ThemeDefinition API — every CSS variable exposed | | Unified autocomplete | One completions array for keywords, snippets, custom symbols, and DSL items | | VS Code-style docs panel | Description panel beside the popup — shown for items with description | | Dynamic provider | provideCompletions callback for context-aware, fully runtime-controlled completions | | Built-in snippets | 17 snippets across TypeScript/JS, CSS, and HTML — Tab-expandable with full tab-stop session ($1$2 → …) | | Emmet expansion | div.wrapper>ul>li*3 → Tab — with inline preview tooltip | | Multi-cursor | Alt+Click to add cursors; Ctrl+D to select next occurrence | | Hover documentation | Tooltip on pointer-rest over any identifier — built-in JS/TS/CSS docs + custom provideHover callback | | Move line | Alt+Up / Alt+Down to move the current line or selected block up/down | | Duplicate line | Shift+Alt+Down / Shift+Alt+Up to duplicate the current line or selected block — cursor follows the new copy | | Go to Line | Ctrl+G / Cmd+G prompt to jump to any line number — opt-in via goToLine: true | | Placeholder | Ghost hint text shown in the editor when the document is empty — set via placeholder option | | Find & Replace | Literal and regex search, case-sensitive mode, replace one / all | | Code folding | Collapse {} blocks via gutter toggle | | Bracket matching | Highlights matching ()[]{} pairs at the cursor | | Word highlight | All occurrences of the word under the cursor highlighted subtly | | Active line highlight | Distinct background on the current line and its gutter cell | | Minimap | Canvas-rendered overview with draggable viewport slider | | Status bar | Language · line/col · selection · undo depth · word-wrap toggle · theme picker | | Indent guides | Faint vertical lines at each indentation level | | Whitespace rendering | Visible · / for spaces and tabs (none / boundary / all) | | Cursor styles | line, block, or underline; configurable blink rate | | Read-only mode | All edits blocked; navigation, selection, and copy still work |


Installation

npm install syncline-editor

Quick Start

ES Module

import { createEditor } from 'syncline-editor';

const editor = createEditor(document.getElementById('app')!, {
  value: 'const greeting = "Hello, world!";',
  language: 'typescript',
  theme: 'dracula',
  onChange: (value) => console.log('changed:', value.length, 'chars'),
});

Give the container an explicit height — the editor fills 100% of its host element:

<div id="app" style="width: 100%; height: 600px;"></div>

UMD / CDN

<script src="syncline-editor.umd.js"></script>
<script>
  const editor = SynclineEditor.createEditor(document.getElementById('app'), {
    value: '// start coding',
    language: 'typescript',
    theme: 'vscode-dark',
  });
</script>

Configuration — Full Reference

Pass any subset of EditorConfig to createEditor(). Every field is optional. All options can also be updated at runtime via editor.updateConfig(patch).

Document

| Option | Type | Default | Description | |---|---|---|---| | value | string \| string[] | '' | Initial document content. String or pre-split string[]. | | language | Language | 'typescript' | Syntax highlighting and autocomplete language. |

Display

| Option | Type | Default | Description | |---|---|---|---| | showGutter | boolean | true | Show/hide the line-number gutter. | | showMinimap | boolean | true | Show/hide the minimap panel. | | showStatusBar | boolean | true | Show/hide the bottom status bar. | | showIndentGuides | boolean | true | Faint vertical lines at each indentation level. | | highlightActiveLine | boolean | true | Background tint on the active (cursor) line and gutter cell. | | wordHighlight | boolean | true | Highlight all other occurrences of the word under the cursor. | | renderWhitespace | 'none' \| 'boundary' \| 'all' | 'none' | Spaces render as ·, tabs as . 'boundary' = leading/trailing only. |

Typography

| Option | Type | Default | Description | |---|---|---|---| | fontFamily | string | "'JetBrains Mono', monospace" | CSS font-family for all code text. | | fontSize | number | 13 | Font size in pixels. | | lineHeight | number | 22 | Row height in pixels — all scroll and minimap calculations derive from this. |

Cursor

| Option | Type | Default | Description | |---|---|---|---| | cursorStyle | 'line' \| 'block' \| 'underline' | 'line' | Visual cursor style. | | cursorBlinkRate | number | 1050 | Blink period in ms. Set to 999999 to disable blinking. |

Layout

| Option | Type | Default | Description | |---|---|---|---| | gutterWidth | number | 60 | Gutter width in pixels. | | minimapWidth | number | 120 | Minimap panel width in pixels. | | wordWrap | boolean | false | Soft-wrap long lines. Toggle at runtime with Alt+Z. | | wrapColumn | number | 80 | Column at which soft-wrap breaks when wordWrap is true. |

Editing

| Option | Type | Default | Description | |---|---|---|---| | tabSize | number | 2 | Spaces per Tab press. | | insertSpaces | boolean | true | Insert spaces on Tab; false inserts a literal \t. | | maxUndoHistory | number | 300 | Maximum undo snapshots retained. | | undoBatchMs | number | 700 | Keystrokes within this window are grouped into one undo step. 0 = per-keystroke. | | readOnly | boolean | false | Block all edits. Navigation, selection, and copy still work. | | autoClosePairs | Record<string,string> | see below | Characters that auto-close. Pass {} to disable entirely. | | lineCommentToken | string | '' | Prefix for Ctrl+/ toggle comment. Auto-detects from language when empty. | | wordSeparators | string | '' | Extra characters treated as word boundaries for double-click and Ctrl+←/→. |

Default autoClosePairs:

{ '(': ')', '[': ']', '{': '}', '"': '"', "'": "'", '`': '`' }

Features Config

| Option | Type | Default | Description | |---|---|---|---| | bracketMatching | boolean | true | Highlight matching ()[]{} pair at the cursor. | | codeFolding | boolean | true | Gutter fold button for collapsible blocks. | | emmet | boolean | true | Emmet abbreviation expansion via Tab. | | snippetExpansion | boolean | true | Tab-expand built-in and custom snippets. | | autocomplete | boolean | true | Show the autocomplete popup while typing. | | autocompletePrefixLength | number | 2 | Minimum characters typed before the popup appears. | | multiCursor | boolean | true | Alt+Click and Ctrl+D multi-cursor. | | find | boolean | true | Enable the find bar (Ctrl+F). | | findReplace | boolean | true | Enable find-and-replace (Ctrl+H). | | wordSelection | boolean | true | Double-click selects the word under the cursor. | | hover | boolean | true | Show a documentation tooltip when the pointer rests on a known identifier for ~500 ms. Covers built-in JS/TS symbols and any symbol in completions with a description, plus the provideHover callback. | | goToLine | boolean | false | Enable the Go to Line bar (Ctrl+G / Cmd+G). Pressing the shortcut opens a centered prompt; Enter jumps the cursor, Escape dismisses. Off by default. | | placeholder | string | '' | Ghost text rendered at the cursor position when the document is completely empty. Hidden when the document has any content or when the string is empty. |

Syntax & Autocomplete Customisation

| Option | Type | Default | Description | |---|---|---|---| | extraKeywords | string[] | [] | Words highlighted as keywords and added to autocomplete. | | extraTypes | string[] | [] | Words highlighted as types and added to autocomplete. | | completions | CompletionItem[] | [] | Unified completions array — symbols, snippets, DSL items, all in one place. | | replaceBuiltins | boolean | false | When true, completions replaces the built-in language keywords/types entirely. | | provideCompletions | (ctx: CompletionContext) => CompletionItem[] \| null | undefined | Dynamic callback — called on every popup open; return null to fall through to defaults. | | provideHover | (ctx: HoverContext) => HoverDoc \| null | undefined | Dynamic hover callback — return a HoverDoc to show a tooltip for any word not covered by built-in docs or the completions array. | | maxCompletions | number | 14 | Maximum items shown in the autocomplete popup. |

Token Colors Config

| Option | Type | Default | Description | |---|---|---|---| | tokenColors | TokenColors | {} | Per-token colour overrides layered on top of the active theme. See Token Colors. |

Theme Config

| Option | Type | Default | Description | |---|---|---|---| | theme | string \| ThemeDefinition | '' (VR Dark) | Built-in theme ID or a full ThemeDefinition object. |

Callbacks

| Option | Signature | Description | |---|---|---| | onChange | (value: string) => void | Fired after every content change (keystroke, paste, undo, setValue). | | onCursorChange | (pos: CursorPosition) => void | Fired when the cursor moves. | | onSelectionChange | (sel: Selection \| null) => void | Fired when the selection changes or clears. | | onFocus | () => void | Fired when the editor gains keyboard focus. | | onBlur | () => void | Fired when the editor loses keyboard focus. | | provideHover | (ctx: HoverContext) => HoverDoc \| null \| undefined | Provides custom hover documentation. Called after built-in lookup and completions search. |

Updating Config at Runtime

Any option can be changed after creation — no reload needed:

// Toggle features instantly
editor.updateConfig({ wordWrap: true, showMinimap: false });

// Switch language (rebuilds highlighting + completions)
editor.updateConfig({ language: 'css' });

// Change font
editor.updateConfig({
  fontFamily: "'Fira Code', monospace",
  fontSize: 14,
  lineHeight: 24,
});

// Enter read-only mode
editor.updateConfig({ readOnly: true });

// Override token colours on top of current theme
editor.updateConfig({
  tokenColors: { keyword: '#ff79c6', string: '#f1fa8c' },
});

// Restore all token colours to theme defaults
editor.updateConfig({ tokenColors: {} });

// Swap completions at runtime
editor.updateConfig({ completions: newCompletions });

Runtime API

All methods on the EditorAPI object returned by createEditor().

Content

editor.getValue(): string              // full document as a newline-joined string
editor.setValue(value: string): void   // replace document (records undo snapshot)

Cursor & Selection

editor.getCursor(): CursorPosition           // { row, col } — zero-based
editor.setCursor(pos: CursorPosition): void  // moves cursor, scrolls into view

editor.getSelection(): Selection | null            // { ar, ac, fr, fc } or null
editor.setSelection(sel: Selection | null): void   // null to deselect

editor.insertText(text: string): void  // insert at cursor position; no-op when readOnly

History

editor.undo(): void
editor.redo(): void

Commands

editor.executeCommand(name: string): void

| Command | Action | Blocked by readOnly | |---|---|---| | 'undo' | Undo | ✓ | | 'redo' | Redo | ✓ | | 'selectAll' | Select all | — | | 'copy' | Copy selection to clipboard | — | | 'cut' | Cut selection | ✓ | | 'paste' | Paste from clipboard | ✓ | | 'toggleComment' | Toggle line comment | ✓ | | 'duplicateLine' | Duplicate current line | ✓ | | 'deleteLine' | Delete current line | ✓ | | 'toggleWordWrap' | Toggle word wrap | — | | 'find' | Open find bar | — | | 'findReplace' | Open find + replace bar | — | | 'indentLine' | Indent selection / current line | ✓ | | 'outdentLine' | Outdent selection / current line | ✓ |

Themes API

editor.setTheme(theme: string | ThemeDefinition): void  // switch by ID or object
editor.getThemes(): string[]                            // all registered theme IDs
editor.registerTheme(theme: ThemeDefinition): void      // register for later use

Config

editor.updateConfig(patch: Partial<EditorConfig>): void

Lifecycle

editor.focus(): void    // programmatically focus the editor
editor.destroy(): void  // unmount all DOM nodes; do not use the instance afterwards

Syntax Highlighting

The editor uses a hand-written, zero-dependency tokeniser that produces nine token classes mapped to CSS custom properties.

Supported Languages

| language | Keywords | Types | Built-in completions | |---|---|---|---| | 'typescript' | 55 keywords (interface, type, readonly, enum, satisfies, …) | 25 types (Promise, Array, HTMLElement, …) | ~50 JS/TS functions | | 'javascript' | 35 keywords (TypeScript-specific syntax excluded) | 17 types | Same ~50 JS functions | | 'css' | 50 value keywords (flex, block, grid, @media, …) | — | 100+ CSS properties + CSS functions | | 'json' | null, true, false | — | — | | 'markdown' | — | — | — | | 'text' | — | — | — |

Token Classes and CSS Variables

| Token class | CSS variable | What it colours | Examples | |---|---|---|---| | kw | --tok-kw | Keywords | const, return, if, flex, @media | | str | --tok-str | Strings | "hello", 'world', `template` | | cmt | --tok-cmt | Comments | // line, /* block */ | | fn | --tok-fn | Functions | console.log(, fetch(, calc( | | num | --tok-num | Numbers | 42, 3.14, 0xff, 1n | | cls | --tok-cls | Classes | MyClass, EventEmitter, Promise | | op | --tok-op | Operators | +, =>, ===, &&, ?., \| | | typ | --tok-typ | Types | string, boolean, HTMLElement | | dec | --tok-dec | Decorators | @Component, @Injectable |

Adding Extra Keywords and Types

extraKeywords and extraTypes affect both syntax highlighting and autocomplete:

createEditor(container, {
  language: 'typescript',
  extraKeywords: ['pipeline', 'stage', 'emit'],
  extraTypes:    ['Observable', 'Subject', 'BehaviorSubject'],
});

Update at runtime (rebuilds the tokeniser cache immediately):

editor.updateConfig({
  extraKeywords: ['pipeline', 'stage', 'emit', 'dispatch'],
});

Token Colors

Override individual syntax token colours on top of any active theme without replacing the whole theme. Pass only the fields you want to change — omitted fields keep the theme default.

Quick Overrides

import type { TokenColors } from 'syncline-editor';

// Override specific tokens — all others remain from the active theme
editor.updateConfig({
  tokenColors: {
    keyword:   '#ff79c6',  // pink keywords
    string:    '#f1fa8c',  // yellow strings
    comment:   '#6272a4',  // muted blue comments
    function:  '#50fa7b',  // green functions
    number:    '#bd93f9',  // purple numbers
    class:     '#8be9fd',  // cyan class names
    operator:  '#f8f8f2',  // near-white operators
    type:      '#8be9fd',  // cyan types
    decorator: '#ffb86c',  // orange decorators
  },
});

// Restore everything to the current theme's defaults
editor.updateConfig({ tokenColors: {} });

// Remove just one override and keep the rest
editor.updateConfig({
  tokenColors: { ...currentOverrides, keyword: '' },
});

TokenColors Fields

| Field | CSS variable | What it highlights | |---|---|---| | keyword | --tok-kw | if, const, class, interface, flex, @media | | string | --tok-str | String and template literals | | comment | --tok-cmt | Line and block comments | | function | --tok-fn | Function names, CSS functions like calc() | | number | --tok-num | Numeric literals | | class | --tok-cls | Class names, constructor calls | | operator | --tok-op | Operators and punctuation | | type | --tok-typ | Type names, built-in types | | decorator | --tok-dec | Decorator annotations (@Component, @Injectable) |

How Token Colour Overrides Work

Token colours are CSS custom properties (--tok-kw, --tok-str, …) written as inline styles on the editor's host element. Both the theme engine and tokenColors write to the same properties:

  1. setTheme() — writes all --tok-* values from the theme definition
  2. updateConfig({ tokenColors }) — overwrites specific properties on top

This means token color overrides survive theme switches — they are automatically re-applied each time the theme changes.

Tip: Prefer tokenColors for quick colour customisation. Use a full ThemeDefinition only when you need to control backgrounds, borders, highlights, and the minimap too.


Themes

Built-in Themes

| ID | Style | |---|---| | '' (empty string) | VR Dark (default) | | 'vscode-dark' | VS Code Dark+ | | 'monokai' | Monokai | | 'dracula' | Dracula | | 'github-light' | GitHub Light | | 'solarized-light' | Solarized Light |

Switching Themes

// By ID
editor.setTheme('dracula');
editor.setTheme('github-light');

// By object (register not required when passing directly)
editor.setTheme(myCustomTheme);

// Via updateConfig
editor.updateConfig({ theme: 'monokai' });

// Get all registered IDs
const ids = editor.getThemes();
// ['', 'vscode-dark', 'monokai', 'dracula', 'github-light', 'solarized-light', ...]

Importing Built-in Theme Objects

import {
  BUILT_IN_THEMES,       // ThemeDefinition[] — all six
  THEME_VR_DARK,
  THEME_VSCODE_DARK,
  THEME_MONOKAI,
  THEME_DRACULA,
  THEME_GITHUB_LIGHT,
  THEME_SOLARIZED_LIGHT,
} from 'syncline-editor';

Creating a Custom Theme

A ThemeDefinition requires an id, name, description, a light flag, and a complete tokens object. Provide all keys to ensure full coverage across every UI surface.

import type { ThemeDefinition } from 'syncline-editor';

const myTheme: ThemeDefinition = {
  id:          'my-purple',
  name:        'My Purple',
  description: 'Custom purple dark theme',
  light:       false,
  tokens: {
    // ── Backgrounds ────────────────────────────────────────────
    bg0: '#0a0a14', bg1: '#0f0f1a', bg2: '#141420',
    bg3: '#1a1a28', bg4: '#20203a',

    // ── Borders ────────────────────────────────────────────────
    border:  'rgba(255,255,255,.08)',
    border2: 'rgba(255,255,255,.14)',
    border3: 'rgba(255,255,255,.22)',

    // ── Text ───────────────────────────────────────────────────
    text: '#e0deff', text2: '#8080c0', text3: '#404060',

    // ── Accent ─────────────────────────────────────────────────
    accent: '#ff79c6', accent2: '#6644aa',

    // ── Semantic colours ───────────────────────────────────────
    green: '#50fa7b', orange: '#ffb86c',
    purple: '#bd93f9', red: '#ff5555', yellow: '#f1fa8c',

    // ── Cursor ─────────────────────────────────────────────────
    cur: '#ff79c6', curGlow: 'rgba(255,121,198,.55)',

    // ── Editor surface ─────────────────────────────────────────
    curLineBg:     'rgba(255,121,198,.04)',
    curLineGutter: '#161628',
    gutterBg:      '#0a0a14',
    gutterHover:   '#161628',
    gutterBorder:  'rgba(255,255,255,.06)',
    gutterNum:     '#404060',
    gutterNumAct:  '#8080c0',

    // ── Highlights ─────────────────────────────────────────────
    selBg:        'rgba(189,147,249,.25)',
    wordHlBg:     'rgba(255,121,198,.07)',
    wordHlBorder: 'rgba(255,121,198,.25)',
    bmBorder:     'rgba(255,121,198,.65)',
    foldBg:       'rgba(255,121,198,.07)',
    foldBorder:   'rgba(255,121,198,.30)',

    // ── Find bar ───────────────────────────────────────────────
    findBg:        'rgba(241,250,140,.14)',
    findBorder:    'rgba(241,250,140,.52)',
    findCurBg:     '#f1fa8c',
    findCurBorder: '#d4dc50',
    findCurText:   '#282a36',

    // ── Sidebar ────────────────────────────────────────────────
    fileActiveBg:   'rgba(255,121,198,.12)',
    fileActiveText: '#ff79c6',

    // ── Minimap ────────────────────────────────────────────────
    mmBg:     '#0a0a14',
    mmSlider: 'rgba(255,255,255,.07)',
    mmDim:    'rgba(0,0,0,.32)',
    mmEdge:   'rgba(255,255,255,.20)',

    // ── Misc ───────────────────────────────────────────────────
    indentGuide: 'rgba(255,255,255,.07)',

    // ── Syntax token colours ───────────────────────────────────
    tokKw:  '#ff79c6', tokStr: '#f1fa8c',
    tokCmt: '#6272a4', tokFn:  '#50fa7b',
    tokNum: '#bd93f9', tokCls: '#8be9fd',
    tokOp:  '#f8f8f2', tokTyp: '#8be9fd',
    tokDec: '#ffb86c',
  },
};

// Register once, then switch by ID from anywhere
editor.registerTheme(myTheme);
editor.setTheme('my-purple');

Extending a Built-in Theme

Spread an existing theme's tokens and override only the fields you need:

import { THEME_DRACULA } from 'syncline-editor';

editor.registerTheme({
  id:    'dracula-tweaked',
  name:  'Dracula (tweaked)',
  description: 'Dracula with warmer keyword and string colours',
  light: false,
  tokens: {
    ...THEME_DRACULA.tokens,   // inherit everything
    tokKw:  '#ffb86c',         // orange keywords instead of pink
    tokStr: '#f1fa8c',         // yellow strings instead of green
  },
});

editor.setTheme('dracula-tweaked');

ThemeTokens Reference

Every key maps to a --<name> CSS custom property on the editor's Shadow DOM host.

| Group | Keys | |---|---| | Backgrounds | bg0 bg1 bg2 bg3 bg4 | | Borders | border border2 border3 | | Text | text text2 text3 | | Accent | accent accent2 | | Semantic | green orange purple red yellow | | Cursor | cur curGlow | | Editor surface | curLineBg curLineGutter gutterBg gutterHover gutterBorder gutterNum gutterNumAct | | Highlights | selBg wordHlBg wordHlBorder bmBorder foldBg foldBorder | | Find bar | findBg findBorder findCurBg findCurBorder findCurText | | Sidebar | fileActiveBg fileActiveText | | Minimap | mmBg mmSlider mmDim mmEdge | | Misc | indentGuide | | Syntax token colours | tokKw tokStr tokCmt tokFn tokNum tokCls tokOp tokTyp tokDec |


Autocomplete

The popup opens when the user has typed at least autocompletePrefixLength characters (default 2) and shows up to maxCompletions items (default 14).

Emmet abbreviations also surface in the same popup with an E badge whenever the current prefix matches a valid abbreviation.

The Unified completions Array

All custom completions — regular symbols, snippets, language-filtered items — go into a single completions array and are differentiated by kind:

import type { CompletionItem } from 'syncline-editor';

createEditor(container, {
  language: 'typescript',
  completions: [
    // Regular function symbol — shows description on the right panel when selected
    {
      label:       'fetchUser',
      kind:        'fn',
      detail:      '(id: string) => Promise<User>',
      description: 'Fetches a user by ID from the REST API.\n\nReturns `null` if the user does not exist.',
    },

    // Type symbol
    {
      label:       'UserStatus',
      kind:        'typ',
      detail:      'enum',
      description: 'Represents the current state of a user account.\n\n`active` | `suspended` | `pending`',
    },

    // Snippet — kind: "snip" + body template
    {
      label:       'mycomp',
      kind:        'snip',
      detail:      'React component scaffold',
      description: 'Scaffolds a named React functional component with JSX return.',
      body:        'export function $1Component() {\n  return (\n    <div>\n      $2\n    </div>\n  );\n}',
      language:    ['typescript'],  // only show in TypeScript files
    },
  ],
});

Update completions at runtime (takes effect on the next popup open):

editor.updateConfig({ completions: updatedItems });

CompletionItem Reference

interface CompletionItem {
  label:        string;             // text inserted on accept; used for prefix matching
  kind:         CompletionKind;     // badge type — see table below
  detail?:      string;             // short hint shown on the right of the popup row
  description?: string;            // full docs shown in the side panel when selected
  body?:        string;             // snippet template; Tab/Enter expands it when set
  language?:    string | string[];  // restrict to specific language(s); omit = all
}

| kind | Badge | Intended use | |---|---|---| | 'kw' | K | Language keyword | | 'fn' | f | Function, method, CSS function | | 'typ' | T | Type, interface, enum | | 'cls' | C | Class name | | 'var' | · | Variable, CSS property, in-file word | | 'snip' | S | Snippet — set body and accepting it expands the template | | 'emmet' | E | Emmet abbreviation (auto-generated, not user-defined) |

Description Panel

When a selected item has a description, a VS Code-style documentation panel opens to the right of the suggestion list. For 'snip' items without an explicit description, the body template is shown as a preview automatically.

{
  label:       'useState',
  kind:        'fn',
  detail:      '<S>(initialState: S) => [S, Dispatch<SetStateAction<S>>]',
  description: 'Returns a stateful value and a setter function.\n\n' +
               'During the initial render the state equals `initialState`.\n\n' +
               'The setter function updates the state and triggers a re-render.',
}

Multi-line descriptions use \n for line breaks. Markdown is not rendered — plain text only.

Replace Built-in Completions — replaceBuiltins

Set replaceBuiltins: true to suppress all built-in language keywords and show only your completions. Perfect for DSL editors where language noise would be confusing:

// SQL editor — hides all JS/TS keywords, shows only SQL
createEditor(container, {
  language:        'text',
  replaceBuiltins: true,
  completions: [
    { label: 'SELECT',   kind: 'kw', detail: 'SQL', description: 'Retrieve rows from a table.' },
    { label: 'FROM',     kind: 'kw', detail: 'SQL' },
    { label: 'WHERE',    kind: 'kw', detail: 'SQL', description: 'Filter rows by a condition.' },
    { label: 'JOIN',     kind: 'kw', detail: 'SQL' },
    { label: 'GROUP BY', kind: 'kw', detail: 'SQL' },
    { label: 'ORDER BY', kind: 'kw', detail: 'SQL' },
    { label: 'LIMIT',    kind: 'kw', detail: 'SQL', description: 'Limit the number of rows returned.' },
  ],
});

Popup Size

createEditor(container, {
  maxCompletions:           20,   // max items shown (default 14)
  autocompletePrefixLength: 1,    // trigger after 1 char (default 2)
});

Priority Order

When multiple sources are configured, the final suggestion list is built in this order:

  1. provideCompletions callback — if it returns a non-null array, it wins entirely and all other sources are skipped
  2. completions with replaceBuiltins: true — your items replace language defaults
  3. completions (default) — merged on top of language defaults
  4. Language built-ins — keywords, types, and functions for the active language
  5. In-file words — identifiers extracted from the current document, always appended last

Hover Documentation

When hover: true (the default), resting the pointer over a recognised identifier for ~500 ms shows a floating tooltip with a title, type signature, and description — exactly like VS Code's hover.

How the lookup works (priority order)

  1. Built-in docs — ~75 entries covering common JS/TS APIs (console.log, Math.floor, Promise.all, fetch, async, const, utility types like Record, Partial, …)
  2. completions array — any item in your completions config that has a description (and/or detail) is automatically available as a hover doc. No extra config needed.
  3. provideHover callback — a runtime function you supply for anything not covered above.

Hover from completions — automatic

If you already define completions with descriptions, hover just works:

const editor = createEditor(container, {
  completions: [
    {
      label: 'myQuery',
      kind: 'fn',
      detail: '(id: string) => Promise<User>',   // → shown as type signature
      description: 'Fetches a user by their ID.\n\nReturns null if the user does not exist.',  // → shown as body
    },
    {
      label: 'MyEntity',
      kind: 'cls',
      detail: 'class MyEntity',
      description: 'The core domain model. Includes all persistence and validation logic.',
    },
  ],
});
// Hovering over "myQuery" or "MyEntity" in the editor now shows a tooltip automatically.

provideHover — fully custom docs

Use provideHover for symbols that live in an external symbol table, API schema, or documentation database:

const editor = createEditor(container, {
  provideHover: (ctx) => {
    // ctx = { word, row, col, line, language, doc }

    // Example: look up from a GraphQL schema
    const field = mySchema.getField(ctx.word);
    if (!field) return null;   // return null to show nothing

    return {
      title: field.name,
      type: field.type,           // e.g. "String!"
      body: field.description,    // e.g. "The unique identifier of this node."
    };
  },
});

HoverDoc reference

interface HoverDoc {
  title: string;       // Bold name at the top of the tooltip
  type?: string;       // Type signature in monospace (optional)
  body: string;        // Description text
}

HoverContext reference

interface HoverContext {
  word: string;        // The identifier under the pointer (may include dot prefix, e.g. "console.log")
  row: number;         // Zero-based document line
  col: number;         // Zero-based character column
  line: string;        // Full text of the hovered line
  language: string;    // Active language (e.g. "typescript")
  doc: readonly string[];  // Full document as array of lines
}

Disable hover entirely

editor.updateConfig({ hover: false });

Snippets

Snippets let you type a short trigger word and press Tab to expand a full multi-line block. After expansion a tab-stop session begins: the cursor lands at $1, and each subsequent Tab advances to $2, $3, and so on. Shift+Tab moves backward. Upcoming tab stops are shown as dim ghost cursor markers so you always know where you'll land next. The session ends when you reach the last stop, press Escape, or click elsewhere.

Snippets appear in the unified autocomplete popup with an S badge alongside keywords, functions, and Emmet abbreviations. When a snippet item is selected in the popup, its body template is previewed in the description panel on the right.

Built-in Snippets by Language

TypeScript / JavaScript

| Trigger | Expands to | |---|---| | fn | function name(params) { } | | afn | const name = (params) => { }; | | asyncfn | async function name(params): Promise<T> { } | | cl | class Name { constructor(params) { } } | | forof | for (const item of iterable) { } | | forin | for (const key in object) { } | | trycatch | try { } catch (error) { } | | promise | new Promise<T>((resolve, reject) => { }) | | imp | import { name } from 'module'; | | iface | interface Name { field: Type; } (TypeScript only) | | ife | Immediately-invoked function expression | | sw | switch (expr) { case val: … default: … } |

CSS

| Trigger | Expands to | |---|---| | flex | Flexbox container (display: flex; align-items; justify-content) | | grid | CSS grid (display: grid; grid-template-columns; gap) | | media | @media (max-width: 768px) { } | | anim | @keyframes name { from { } to { } } | | var | --name: value; |

HTML / General (available in all languages)

| Trigger | Expands to | |---|---| | accordion | <details><summary>…</summary><div>…</div></details> | | card | Card with header, body, and footer divs | | navbar | <nav> with anchor links | | modal | Modal dialog with header, body, and footer | | table | <table> with <thead> and <tbody> |

Custom Snippets

Add your own snippets to the unified completions array with kind: 'snip' and a body template. They appear in the popup with an S badge and also expand on Tab when the label is typed exactly:

import type { CompletionItem } from 'syncline-editor';

createEditor(container, {
  language: 'typescript',
  completions: [
    {
      label:       'rcomp',
      kind:        'snip',
      detail:      'React component',
      description: 'Scaffolds a typed React functional component.',
      body:        'export function $1({ $2 }: $1Props) {\n  return (\n    <div>\n      $3\n    </div>\n  );\n}',
      language:    ['typescript'],
    },
    {
      label:  'clog',
      kind:   'snip',
      detail: 'console.log with label',
      body:   "console.log('$1:', $2);",
    },
    {
      label:  'todo',
      kind:   'snip',
      detail: 'TODO comment',
      body:   '// TODO($1): $2',
    },
  ],
});

// Add more at runtime
editor.updateConfig({ completions: [...existing, newItem] });

Disable all snippet expansion (built-in and custom):

createEditor(container, { snippetExpansion: false });

Snippet Body Syntax

| Placeholder | Meaning | |---|---| | $1 | First tab stop — cursor lands here immediately after expansion | | $2, $3, … | Additional tab stops — press Tab to jump through them in order | | $0 | Final cursor position (VS Code convention) — session ends here | | \n | Line break — continuation lines inherit the trigger line's indentation |

Tab stops are rendered as dim ghost cursor markers at their positions in the document. As you type at the current stop the ghost markers shift to stay accurate. Pressing Shift+Tab moves back to the previous stop. The session exits when you reach $0 (or the last stop), press Escape, click elsewhere, or press Enter / Delete.

Priority: Emmet is tried first on Tab. If both Emmet and a snippet match the same word, Emmet wins.


Emmet

Type an abbreviation and press Tab to expand. A preview tooltip appears above the cursor while a valid abbreviation is detected.

ul>li*5              →  <ul><li></li> × 5</ul>
div.container        →  <div class="container"></div>
input[type=text]     →  <input type="text">
a:href               →  <a href=""></a>
section>h2+p         →  <section><h2></h2><p></p></section>

Emmet abbreviations also appear in the autocomplete popup with an E badge — select and press Tab or Enter to expand.

createEditor(container, { emmet: false }); // disable

Dynamic Completion Provider

Use provideCompletions for fully context-aware completions that change based on the cursor position, current prefix, or document content. When this callback returns a non-null array, it completely overrides all other completion sources.

provideCompletions

import type { CompletionContext, CompletionItem } from 'syncline-editor';

createEditor(container, {
  provideCompletions: (ctx: CompletionContext): CompletionItem[] | null => {
    // ctx.prefix   — characters typed before the cursor on the current line
    // ctx.language — active language string ('typescript', 'css', etc.)
    // ctx.line     — zero-based cursor row
    // ctx.col      — zero-based cursor column
    // ctx.doc      — full document as string[] (one element per line)

    // Example: variable completions after "$"
    if (ctx.prefix.startsWith('$')) {
      return myVariableTable.map(v => ({
        label:       v.name,
        kind:        'var' as const,
        detail:      v.type,
        description: v.docs,
      }));
    }

    // Example: import suggestions inside import statements
    const importLine = ctx.doc[ctx.line];
    if (importLine.startsWith('import ') && ctx.prefix.length >= 1) {
      return myModuleList.map(m => ({
        label: m.name,
        kind:  'cls' as const,
        detail: m.version,
      }));
    }

    // Return null to fall through to built-in completions
    return null;
  },
});

Update the provider at runtime:

editor.updateConfig({
  provideCompletions: (ctx) => newProvider(ctx),
});

CompletionContext

interface CompletionContext {
  prefix:   string;    // characters typed before the cursor (the match prefix)
  language: string;    // active language ID
  line:     number;    // cursor row, zero-based
  col:      number;    // cursor column, zero-based
  doc:      string[];  // full document split into lines
}

Events & Callbacks

All callbacks are defined in EditorConfig and can be updated via updateConfig at any time:

const editor = createEditor(container, {
  onChange: (value) => {
    // Fires after every edit — keystroke, paste, undo, setValue(), …
    // value: full document as a newline-joined string
    autoSave(value);
  },

  onCursorChange: (pos) => {
    // pos.row and pos.col are zero-based
    statusEl.textContent = `Ln ${pos.row + 1}, Col ${pos.col + 1}`;
  },

  onSelectionChange: (sel) => {
    // sel is null when the selection is cleared
    if (sel) {
      const rows = Math.abs(sel.fr - sel.ar) + 1;
      console.log(`${rows} line(s) selected`);
    }
  },

  onFocus: () => container.classList.add('editor-focused'),
  onBlur:  () => container.classList.remove('editor-focused'),
});

// Replace a callback at runtime — no re-creation needed
editor.updateConfig({
  onChange: (value) => newAutoSave(value),
});

Advanced Features

Multi-cursor

| Action | Shortcut | |---|---| | Add cursor at click position | Alt / Option + Click | | Select next occurrence of word | Ctrl / Cmd + D | | Clear all extra cursors | Escape |

Every cursor has its own independent selection. All cursors type, delete, and move in sync.

createEditor(container, { multiCursor: false }); // disable

Find & Replace

Open programmatically:

editor.executeCommand('find');         // opens find bar
editor.executeCommand('findReplace');  // opens with replace row visible

The find bar supports:

  • Match caseAa toggle
  • Regex mode.* toggle
  • Navigate / buttons or Enter / Shift+Enter
  • Replace one — replaces the current highlighted match
  • Replace all — replaces every match in the document

Configuration options:

// Disable both find and replace
createEditor(container, { find: false });

// Find only — no replace row
createEditor(container, { find: true, findReplace: false });

Code Folding

Click the / gutter toggle next to any foldable block. Folded sections show a dashed bottom border and a indicator.

createEditor(container, { codeFolding: false }); // disable

Bracket Matching

When the cursor is adjacent to (, ), [, ], {, or }, both the opening and closing characters are highlighted with a border.

createEditor(container, { bracketMatching: false }); // disable

Colour controlled by ThemeTokens.bmBorder.

Word Highlight

When the cursor rests on a word, all other occurrences in the document are subtly boxed. Automatically disabled while a selection is active.

createEditor(container, { wordHighlight: false }); // disable

Colours: ThemeTokens.wordHlBg (fill) and ThemeTokens.wordHlBorder (outline).

Whitespace Rendering

createEditor(container, { renderWhitespace: 'none' });      // default — invisible
createEditor(container, { renderWhitespace: 'boundary' });  // leading/trailing only
createEditor(container, { renderWhitespace: 'all' });       // every space and tab

Spaces render as · and tabs as .

Cursor Styles

createEditor(container, { cursorStyle: 'line' });       // thin vertical beam (default)
createEditor(container, { cursorStyle: 'block' });      // filled block behind character
createEditor(container, { cursorStyle: 'underline' });  // horizontal bar below character

createEditor(container, { cursorBlinkRate: 2000 });     // slower blink
createEditor(container, { cursorBlinkRate: 999999 });   // no blink

Minimap

Canvas-rendered pixel-accurate overview of the full document with a draggable viewport slider. Scroll, drag the slider, or click anywhere on the minimap to jump.

createEditor(container, {
  showMinimap:  true,
  minimapWidth: 100,  // pixels (default 120)
});

Colours: ThemeTokens.mmBg, mmSlider, mmDim, mmEdge.

Word Wrap

createEditor(container, {
  wordWrap:   true,
  wrapColumn: 100,  // default 80
});

// Toggle at runtime — also available via Alt+Z keyboard shortcut
editor.executeCommand('toggleWordWrap');
editor.updateConfig({ wordWrap: !currentWrap });

Indent Guides

Faint vertical lines connecting matching indentation levels:

createEditor(container, { showIndentGuides: false }); // disable

Colour controlled by ThemeTokens.indentGuide.

Active Line Highlight

The row containing the cursor gets a distinct background and gutter colour:

createEditor(container, { highlightActiveLine: false }); // disable

Colours: ThemeTokens.curLineBg (row background) and ThemeTokens.curLineGutter (gutter cell).

Read-Only Mode

const viewer = createEditor(container, {
  value:    sourceCode,
  readOnly: true,
});

// These are all silently no-ops in read-only mode:
viewer.insertText('test');
viewer.executeCommand('cut');
viewer.executeCommand('deleteLine');

// These still work normally:
viewer.getCursor();
viewer.getSelection();
viewer.executeCommand('copy');
viewer.executeCommand('find');

// Toggle at runtime:
editor.updateConfig({ readOnly: false });  // re-enable editing
editor.updateConfig({ readOnly: true });   // lock again

Behavioral Options

Auto-Close Pairs

// Default — close all bracket and quote types
createEditor(container, {
  autoClosePairs: { '(': ')', '[': ']', '{': '}', '"': '"', "'": "'", '`': '`' },
});

// Only parentheses and curly braces
createEditor(container, {
  autoClosePairs: { '(': ')', '{': '}' },
});

// Disable auto-closing entirely
createEditor(container, { autoClosePairs: {} });

Typing the opening character inserts the closing character and places the cursor between them. Typing the closing character again skips over it.

Line Comment Token

Controls the Ctrl+/ toggle-comment prefix:

// Auto-detect from language (default — // for TS/JS/CSS, nothing for JSON/Markdown)
createEditor(container, { lineCommentToken: '' });

// Python / Ruby / YAML / shell
createEditor(container, { lineCommentToken: '#' });

// SQL / Lua
createEditor(container, { lineCommentToken: '--' });

// LaTeX
createEditor(container, { lineCommentToken: '%' });

Word Separators

Characters treated as word boundaries for double-click selection, Ctrl+Left/Right, and Ctrl+D:

// Default — use built-in \w word boundary
createEditor(container, { wordSeparators: '' });

// Include hyphens (good for CSS, kebab-case identifiers)
createEditor(container, {
  wordSeparators: '`~!@#%^&*()-=+[{}]\\|;:\'",.<>/?',
});

Undo Batch Window

Controls how keystrokes are grouped into undo steps:

// Default — group keystrokes within 700 ms into one undo step
createEditor(container, { undoBatchMs: 700 });

// Per-keystroke undo — every character is its own step
createEditor(container, { undoBatchMs: 0 });

// One undo per ~2 seconds of continuous typing
createEditor(container, { undoBatchMs: 2000 });

Feature Flags

Disable individual features at creation or toggle them at runtime:

createEditor(container, {
  bracketMatching:     false,  // no bracket-pair highlight
  codeFolding:         false,  // no fold buttons in gutter
  emmet:               false,  // no Emmet expansion
  snippetExpansion:    false,  // no snippet Tab-expansion
  autocomplete:        false,  // no completion popup
  multiCursor:         false,  // no Alt+Click / Ctrl+D
  find:                false,  // no find bar
  findReplace:         false,  // no replace row
  wordSelection:       false,  // double-click places cursor only
  wordHighlight:       false,  // no occurrence boxes
  highlightActiveLine: false,  // no active-line background
  showIndentGuides:    false,  // no indent guide lines
  showGutter:          false,  // hide line numbers
  showMinimap:         false,  // hide minimap panel
  showStatusBar:       false,  // hide status bar
  goToLine:            true,   // enable Ctrl+G / Cmd+G Go to Line
  placeholder:         'Start typing…',  // shown when document is empty
});

Keyboard Shortcuts

| Shortcut | Action | Controlled by | |---|---|---| | Ctrl / Cmd + Z | Undo | undoBatchMs, maxUndoHistory | | Ctrl / Cmd + Y or Ctrl + Shift + Z | Redo | — | | Ctrl / Cmd + A | Select all | — | | Ctrl / Cmd + C | Copy | — | | Ctrl / Cmd + X | Cut | readOnly | | Ctrl / Cmd + V | Paste | readOnly | | Ctrl / Cmd + F | Open find bar | find | | Ctrl / Cmd + H | Open find + replace | findReplace | | Ctrl / Cmd + G | Open Go to Line prompt | goToLine | | Ctrl / Cmd + D | Select next occurrence | multiCursor | | Ctrl / Cmd + Shift + D | Duplicate line | readOnly | | Ctrl / Cmd + K | Delete line | readOnly | | Ctrl / Cmd + / | Toggle line comment | lineCommentToken, readOnly | | Alt / Option + Z | Toggle word wrap | — | | Alt / Option + ↑ | Move current line (or selected block) up | readOnly | | Alt / Option + ↓ | Move current line (or selected block) down | readOnly | | Shift + Alt / Option + ↓ | Duplicate current line (or selected block) down — cursor moves to new copy | readOnly | | Shift + Alt / Option + ↑ | Duplicate current line (or selected block) up — cursor stays on new copy | readOnly | | Tab | Advance to next snippet tab stop · expand Emmet · expand snippet · indent | snippetExpansion, emmet, tabSize | | Shift + Tab | Go to previous snippet tab stop · outdent | snippetExpansion, tabSize | | Alt / Option + → (Mac) | Word skip right (groups by character class: word / punctuation) | wordSeparators | | Alt / Option + ← (Mac) | Word skip left (groups by character class: word / punctuation) | wordSeparators | | Alt / Option + Click | Add extra cursor | multiCursor | | Double-click | Select word | wordSelection, wordSeparators | | Triple-click | Select entire line | — | | Escape | Clear selection · exit snippet session · close popup · close find · remove extra cursors | — | | ↑ ↓ ← → | Move cursor | — | | Cmd + ← → (Mac) | Start / end of line | — | | Cmd + ↑ ↓ (Mac) | Start / end of document | — | | Ctrl + ← → (Win) | Word skip left / right | wordSeparators | | Ctrl + Home / End (Win) | Start / end of document | — | | Option + ← → (Mac) | Word skip left / right | wordSeparators | | Shift + arrow keys | Extend selection | — | | Shift + Cmd/Ctrl + arrow | Extend selection to boundary | — | | Home | First non-whitespace (press again = column 0) | — | | End | End of line | — | | PageUp / PageDown | Scroll one viewport | — | | Backspace | Delete character left | readOnly | | Delete | Delete character right | readOnly |

All mutating shortcuts are silently blocked when readOnly: true. Navigation, selection, and Ctrl+C always work.


Recipes

Real-world patterns you can copy and adapt.

Monaco-style Editor Embed

A feature-complete editor inside a fixed-height container, closely resembling a VS Code embed:

import { createEditor } from 'syncline-editor';

const editor = createEditor(document.getElementById('editor')!, {
  value:               initialCode,
  language:            'typescript',
  theme:               'vscode-dark',
  fontSize:            13,
  lineHeight:          20,
  fontFamily:          "'JetBrains Mono', 'Fira Code', monospace",
  tabSize:             2,
  insertSpaces:        true,
  wordWrap:            false,
  showMinimap:         true,
  showGutter:          true,
  showStatusBar:       true,
  showIndentGuides:    true,
  bracketMatching:     true,
  codeFolding:         true,
  emmet:               true,
  autocomplete:        true,
  multiCursor:         true,
  find:                true,
  findReplace:         true,
  wordHighlight:       true,
  highlightActiveLine: true,
  renderWhitespace:    'boundary',
  onChange:  (v) => localStorage.setItem('draft', v),
  onFocus:   () => console.log('editor focused'),
});
#editor { width: 100%; height: 100vh; }

DSL / SQL Editor

Replace all built-in completions with domain-specific keywords — zero JS/TS noise:

import { createEditor } from 'syncline-editor';
import type { CompletionItem } from 'syncline-editor';

const sqlCompletions: CompletionItem[] = [
  { label: 'SELECT',      kind: 'kw', detail: 'SQL',  description: 'Retrieve rows from a table or view.' },
  { label: 'FROM',        kind: 'kw', detail: 'SQL',  description: 'Specify the source table.' },
  { label: 'WHERE',       kind: 'kw', detail: 'SQL',  description: 'Filter rows using a predicate.' },
  { label: 'JOIN',        kind: 'kw', detail: 'SQL' },
  { label: 'LEFT JOIN',   kind: 'kw', detail: 'SQL' },
  { label: 'INNER JOIN',  kind: 'kw', detail: 'SQL' },
  { label: 'GROUP BY',    kind: 'kw', detail: 'SQL' },
  { label: 'ORDER BY',    kind: 'kw', detail: 'SQL' },
  { label: 'HAVING',      kind: 'kw', detail: 'SQL' },
  { label: 'LIMIT',       kind: 'kw', detail: 'SQL' },
  { label: 'OFFSET',      kind: 'kw', detail: 'SQL' },
  { label: 'INSERT INTO', kind: 'kw', detail: 'SQL' },
  { label: 'UPDATE',      kind: 'kw', detail: 'SQL' },
  { label: 'DELETE FROM', kind: 'kw', detail: 'SQL' },
  { label: 'COUNT',       kind: 'fn', detail: 'aggregate', description: 'COUNT(*) — number of rows.' },
  { label: 'SUM',         kind: 'fn', detail: 'aggregate' },
  { label: 'AVG',         kind: 'fn', detail: 'aggregate' },
  { label: 'MAX',         kind: 'fn', detail: 'aggregate' },
  { label: 'MIN',         kind: 'fn', detail: 'aggregate' },
];

createEditor(container, {
  language:        'text',
  replaceBuiltins: true,
  completions:     sqlCompletions,
  lineCommentToken: '--',
  theme:           'vscode-dark',
});

Read-Only Code Viewer

A zero-interaction code display with syntax highlighting, no editing, no cursors:

createEditor(container, {
  value:               sourceCode,
  language:            'typescript',
  theme:               'github-light',
  readOnly:            true,
  autocomplete:        false,
  emmet:               false,
  snippetExpansion:    false,
  find:                false,
  findReplace:         false,
  multiCursor:         false,
  codeFolding:         false,
  showStatusBar:       false,
  showMinimap:         false,
  highlightActiveLine: false,
  wordHighlight:       false,
  bracketMatching:     false,
  cursorBlinkRate:     999999,
});

Custom Theme from Scratch (Recipe)

Minimal theme extending Dracula with a brand accent colour:

import { THEME_DRACULA } from 'syncline-editor';

const brandTheme = {
  id:          'brand-dark',
  name:        'Brand Dark',
  description: 'Company brand colour scheme',
  light:       false,
  tokens: {
    ...THEME_DRACULA.tokens,
    accent:       '#00C2FF',   // brand blue accent
    accent2:      '#005F7A',
    cur:          '#00C2FF',
    curGlow:      'rgba(0,194,255,.45)',
    selBg:        'rgba(0,194,255,.20)',
    wordHlBorder: 'rgba(0,194,255,.35)',
    tokKw:        '#00C2FF',   // brand-coloured keywords
  },
};

editor.registerTheme(brandTheme);
editor.setTheme('brand-dark');

Framework-aware Autocomplete

Use provideCompletions to serve different items based on context — React hooks inside .tsx, lifecycle methods inside class components, etc.:

import type { CompletionContext, CompletionItem } from 'syncline-editor';

const reactHooks: CompletionItem[] = [
  { label: 'useState',    kind: 'fn', detail: 'React hook', description: 'Adds local state to a functional component.' },
  { label: 'useEffect',   kind: 'fn', detail: 'React hook', description: 'Run side-effects after render.' },
  { label: 'useCallback', kind: 'fn', detail: 'React hook' },
  { label: 'useMemo',     kind: 'fn', detail: 'React hook' },
  { label: 'useRef',      kind: 'fn', detail: 'React hook' },
  { label: 'useContext',  kind: 'fn', detail: 'React hook' },
];

createEditor(container, {
  language: 'typescript',
  provideCompletions: (ctx: CompletionContext): CompletionItem[] | null => {
    // Serve React hooks when prefix starts with "use"
    if (ctx.prefix.startsWith('use')) {
      return reactHooks.filter(h => h.label.startsWith(ctx.prefix));
    }
    return null;  // fall through to built-in completions
  },
});

Auto-Save with Debounce

Debounce onChange so the backend isn't hammered on every keystroke:

function debounce<T extends (...args: any[]) => void>(fn: T, ms: number): T {
  let timer: ReturnType<typeof setTimeout>;
  return ((...args) => {
    clearTimeout(timer);
    timer = setTimeout(() => fn(...args), ms);
  }) as T;
}

const save = debounce(async (value: string) => {
  await fetch('/api/save', {
    method:  'POST',
    headers: { 'Content-Type': 'application/json' },
    body:    JSON.stringify({ content: value }),
  });
}, 1000);

createEditor(container, {
  onChange: save,
});

TypeScript Types

All types are exported from the package root:

import type {
  // Core
  EditorConfig,
  EditorAPI,
  Language,

  // Positions
  CursorPosition,
  Selection,
  FindMatch,
  ExtraCursor,

  // Autocomplete
  CompletionItem,
  CompletionKind,
  CompletionContext,

  // Token colours
  TokenColors,

  // Themes
  ThemeDefinition,
  ThemeTokens,
} from 'syncline-editor';

Language

type Language =
  | 'typescript'
  | 'javascript'
  | 'css'
  | 'json'
  | 'markdown'
  | 'text';

CursorPosition

interface CursorPosition {
  row: number;  // zero-based line index
  col: number;  // zero-based character offset
}

Selection

interface Selection {
  ar: number;  // anchor row — where selection started
  ac: number;  // anchor col
  fr: number;  // focus row  — where the caret currently sits
  fc: number;  // focus col
}

Either end can come before the other — backward selections are valid.

CompletionItem

interface CompletionItem {
  label:        string;             // text inserted on accept; used for prefix matching
  kind:         CompletionKind;     // 'kw' | 'fn' | 'typ' | 'cls' | 'var' | 'snip' | 'emmet'
  detail?:      string;             // short hint shown on the right of the popup row
  description?: string;            // full docs shown in the side panel when this item is selected
  body?:        string;             // snippet template — Tab/Enter expands when set
  language?:    string | string[];  // restrict to specific language(s); omit = all languages
}

CompletionKind

type CompletionKind = 'kw' | 'fn' | 'typ' | 'cls' | 'var' | 'snip' | 'emmet';

CompletionContext

interface CompletionContext {
  prefix:   string;    // characters before the cursor (the match prefix)
  language: string;    // active language ID
  line:     number;    // cursor row (zero-based)
  col:      number;    // cursor column (zero-based)
  doc:      string[];  // full document split into lines
}

TokenColors

interface TokenColors {
  keyword?:   string;  // --tok-kw  — if, const, class, interface, @media
  string?:    string;  // --tok-str — string and template literals
  comment?:   string;  // --tok-cmt — line and block comments
  function?:  string;  // --tok-fn  — function names, CSS functions
  number?:    string;  // --tok-num — numeric literals
  class?:     string;  // --tok-cls — class names, constructor calls
  operator?:  string;  // --tok-op  — +, =>, ===, &&, ?.
  type?:      string;  // --tok-typ — type names, built-in types
  decorator?: string;  // --tok-dec — @Component, @Injectable
}

Pass an empty string '' for any field to restore that token to the current theme's default.

ThemeDefinition

interface ThemeDefinition {
  id:          string;       // unique identifier — used with setTheme() and getThemes()
  name:        string;       // human-readable display name
  description: string;       // short description
  light:       boolean;      // true for light themes (affects status-bar icon tinting)
  tokens:      ThemeTokens;  // complete set of CSS variable values
}

Project Structure

syncline-editor/
├── src/
│   ├── core/
│   │   ├── constants.ts          # Per-language keyword/type sets + layout constants
│   │   ├── document.ts           # Document model, undo/redo, selection helpers
│   │   ├── tokeniser.ts          # Language-aware syntax tokeniser (zero deps)
│   │   └── wrap-map.ts           # Soft-wrap virtual line map
│   ├── features/
│   │   ├── autocomplete.ts       # Completion engine — ranking, filtering, snippet items
│   │   ├── bracket-matcher.ts    # Bracket-pair finder
│   │   ├── code-folding.ts       # Block folding logic
│   │   ├── emmet.ts              # Emmet abbreviation expander
│   │   ├── find-replace.ts       # Find / replace engine
│   │   ├── multi-cursor.ts       # Multi-cursor state manager
│   │   ├── snippets.ts           # Built-in snippet library (TS/JS, CSS, HTML)
│   │   └── word-highlight.ts     # Same-word occurrence finder
│   ├── renderer/
│   │   ├── minimap-renderer.ts   # Canvas minimap + scroll calculations
│   │   └── row-renderer.ts       # Virtual row → HTML string
│   ├── themes/
│   │   ├── built-in-themes.ts    # 6 built-in ThemeDefinition objects
│   │   └── theme-manager.ts      # Theme registry + CSS variable injection
│   ├── types/
│   │   └── index.ts              # All public TypeScript interfaces and types
│   ├── ui/
│   │   └── styles.ts             # All CSS injected into Shadow DOM
│   ├── utils/
│   │   ├── dom.ts                # DOM helpers
│   │   ├── string.ts             # String utilities (word boundaries, escaping, …)
│   │   └── validation.ts         # Config validation helpers
│   ├── SynclineEditor.ts         # Main editor class — implements EditorAPI
│   └── index.ts                  # Public API barrel export
├── playground/
│   └── index.html                # Interactive playground (every option exposed)
├── tests/                        # Vitest test suite
├── package.json
├── tsconfig.json
├── vite.config.ts                # Playground dev server + build
├── vite.lib.config.ts            # Library build (ES module + UMD + .d.ts)
└── README.md

Development

# Install dependencies
npm install

# Start the interactive playground (Vite dev server, hot reload)
npm run dev

# Type-check only (no emit)
npm run typecheck

# Build the playground into dist/
npm run build

# Build the distributable library (ES + UMD + .d.ts)
npm run build:lib

# Preview the built playground
npm run preview

# Run the test suite
npm run test

Playground

The playground at playground/index.html is a fully self-contained interactive demo with every option wired to live controls:

  • Theme switcher — all 6 built-in themes as clickable chips
  • Language selector — TypeScript · JavaScript · CSS · JSON · Markdown · plain text
  • Token Colors — 9 colour pickers with live preview, enable/disable toggle, and reset
  • Features — checkbox for every boolean option
  • Typography — font family, size, line height, cursor style, whitespace rendering
  • Layout — gutter width, minimap width, wrap column
  • AutocompleteprovideCompletions code editor, extra keywords/types, unified completions JSON editor with live validation, replaceBuiltins toggle
  • Behavior — auto-close pairs, line comment token, word separators, undo batch window
  • Actions — buttons for every executeCommand and getValue / setValue
  • Event log — live feed of all onChange / onCursorChange / onSelectionChange / onFocus / onBlur events

License

MIT