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

@editora/light-code-editor

v1.1.0

Published

Lightweight code editor for React and web apps with syntax highlighting, search/replace, extensions, and enterprise SaaS integration patterns.

Readme

Features

  • Extensions, not a monolith. Line numbers, highlighting, search, diagnostics, completions, formatting, folding and the rest are extensions you add, so you ship only what you use.
  • Syntax highlighting for more than twenty languages, switched at run time.
  • Search and replace with case, whole-word and regular-expression modes.
  • Diagnostics, completions and code actions that you feed from your own language service, including async ones that are cancelled when they go stale.
  • Formatting, comments and line commands: format the document or the selection, toggle comments, duplicate and move lines, go to a line.
  • Folding, bracket matching, indent guides and an active-line highlight.
  • Decorations: tint lines, put badges in the gutter and mark ranges of text, without rewriting the text DOM.
  • Light and dark themes, a read-only mode, and a keymap you can change.
  • Accessible by default. The editing surface is a named multi-line text box; the completion list is a combobox listbox (aria-activedescendant, a live status line); find, go to line and the menus are named and keyboard operable; text keeps 4.5:1 in both themes. Checked with axe in Chromium, Firefox and WebKit.
  • Written in TypeScript, no dependencies. Checked in current Chromium, Firefox and WebKit.

Install

npm install @editora/light-code-editor

Quick start

import {
  ActiveLineAndIndentGuidesExtension,
  BracketMatchingExtension,
  CodeFoldingExtension,
  CompletionExtension,
  ContextMenuExtension,
  DiagnosticsExtension,
  EditingCommandsExtension,
  FormattingExtension,
  LineNumbersExtension,
  SearchExtension,
  SyntaxHighlightingExtension,
  createEditor,
} from '@editora/light-code-editor';
import '@editora/light-code-editor/light-code-editor.css';

const container = document.getElementById('editor');

if (!container) {
  throw new Error('Missing #editor container');
}

const editor = createEditor(container, {
  value: `interface User {
  id: number;
  name: string;
}

function greet(user: User): string {
  if (!user.name) {
    return 'Hello, stranger';
  }

  return \`Hello, \${user.name}\`;
}

console.log(greet({ id: 1, name: 'Ada' }));
`,
  theme: 'dark',
  tabSize: 2,
  lineNumbers: true,
  extensions: [
    new LineNumbersExtension(),
    new SyntaxHighlightingExtension(),
    new SearchExtension(),
    new DiagnosticsExtension(),
    new CompletionExtension(),
    new FormattingExtension(),
    new ContextMenuExtension(),
    new EditingCommandsExtension(),
    new ActiveLineAndIndentGuidesExtension(),
    new BracketMatchingExtension(),
    new CodeFoldingExtension(),
  ],
});

editor.executeCommand('setSyntaxLanguage', 'typescript');

editor.on('change', (changes) => {
  console.log('Content changed:', changes);
});

The editor is at least 400 px tall; give the container a height, as in the React example below, to size it, and the editor scrolls inside it. Import the stylesheet once; it styles the gutter, the popups, the panels and the decorations. Call editor.destroy() when you are done with the editor.

Use it in React

There is no React wrapper to learn: create the editor in an effect and destroy it in the cleanup.

import { useEffect, useRef, useState } from 'react';
import {
  LineNumbersExtension,
  SearchExtension,
  SyntaxHighlightingExtension,
  createEditor,
  type EditorAPI,
} from '@editora/light-code-editor';
import '@editora/light-code-editor/light-code-editor.css';

type CodeEditorProps = { value: string; onChange: (value: string) => void; language?: string };

export function CodeEditor({ value, onChange, language = 'json' }: CodeEditorProps) {
  const container = useRef<HTMLDivElement>(null);
  const editor = useRef<EditorAPI | null>(null);
  const latestOnChange = useRef(onChange);
  latestOnChange.current = onChange;

  // Create the editor once, and destroy it when the component goes away.
  useEffect(() => {
    const instance = createEditor(container.current as HTMLElement, {
      value,
      lineNumbers: true,
      extensions: [new LineNumbersExtension(), new SyntaxHighlightingExtension(), new SearchExtension()],
    });
    instance.executeCommand('setSyntaxLanguage', language);
    instance.on('change', () => latestOnChange.current(instance.getValue()));
    editor.current = instance;
    return () => instance.destroy();
  }, []);

  // Copy a value that changed outside the editor into it, but not the editor's own edits.
  useEffect(() => {
    if (editor.current && editor.current.getValue() !== value) editor.current.setValue(value);
  }, [value]);

  return <div ref={container} style={{ height: 180 }} />;
}

export default function App() {
  const [config, setConfig] = useState('{\n  "name": "my-app",\n  "private": true\n}\n');

  return (
    <>
      <CodeEditor value={config} onChange={setConfig} />
      <p>{config.length} characters</p>
      <button onClick={() => setConfig('{}\n')}>Reset</button>
    </>
  );
}

This works in React's StrictMode, which runs effects twice in development.

Languages

Set the language with editor.executeCommand('setSyntaxLanguage', id), or syntax.registerMode(id, mode) to add your own. The default is html.

| Language | Ids | | --- | --- | | HTML, CSS, XML | html, css, xml, svg | | JavaScript, TypeScript | javascript js, jsx, typescript ts, tsx | | JSON, YAML, Markdown | json, yaml yml, markdown md | | Shell | bash shell sh zsh | | Python, Ruby, PHP | python py, ruby rb, php | | Go, Rust, C, C++ | go golang, rust rs, c, cpp c++ cc cxx | | Java, C# | java, csharp cs | | SQL, Dockerfile | sql, dockerfile docker | | Templates | vue, ejs |

Options

createEditor(container, {
  value: '',             // the starting text
  ariaLabel: 'Code editor', // what a screen reader calls the editing surface; name it for what it edits
  theme: 'light',        // 'light' or 'dark'; change it later with setTheme()
  readOnly: false,
  tabSize: 2,            // how wide a tab is drawn
  lineWrapping: false,   // wrap long lines instead of scrolling sideways
  lineNumbers: true,     // the gutter; toggle it later with the toggleLineNumbers command
  extensions: [],        // see Extensions below
  keymap: {},            // your own shortcuts, for the default keymap
});

The editor API

createEditor returns an editor with these methods:

| Method | | | --- | --- | | getValue() / setValue(text) | Read or replace the text. | | getCursor() / setCursor(position) | The caret; a position is { line, column }, both starting at 0. | | getSelection() / setSelection(range) | The selected range, { start, end }, or undefined. | | setTheme(name) / setReadOnly(boolean) | Change the theme or the read-only mode. | | executeCommand(name, ...args) | Run a command (see Extensions). | | registerCommand(name, handler) | Add a command. | | addExtension(ext) / removeExtension(name) | Add or remove an extension later. | | search(query, options?) / replace(range, text) / replaceAll(query, text, options?) | Search and edit by code. | | fold(range) / unfold(range) / getFolds() | Folding. | | setDecorations(layer, list) / clearDecorations(layer?) / getDecorations(layer?) | Decorations. | | focus() / blur() / destroy() | Focus, blur, and tear the editor down. | | on(event, handler) / off(event, handler) | Events. |

| Event | Handler receives | | --- | --- | | change | The list of text changes. | | cursor | The cursor. | | selection | The selected range, or undefined. | | focus, blur | Nothing. | | keydown, mousedown, contextmenu | The DOM event. | | save | Nothing: Cmd/Ctrl+S was pressed. |

Extensions

Pass extensions in extensions. Each one adds commands, which you run with editor.executeCommand(name). The common ones are also bound to keys.

| Extension | What it does | Options | Commands | | --- | --- | --- | --- | | LineNumbersExtension | The line-number gutter. | | toggleLineNumbers | | SyntaxHighlightingExtension | Syntax colours; the active language. | | setSyntaxLanguage | | ThemeExtension | Switch theme at run time. | | setTheme, toggleTheme | | ReadOnlyExtension | A read-only mode. | | setReadOnly, toggleReadOnly | | SearchExtension | Find and replace, with case, whole-word and regular-expression modes. | replaceAndFindNext | find, findNext, findPrev, replace, replaceAll | | DiagnosticsExtension | Gutter markers, inline highlights, issue navigation and a status box. | diagnostics, maxDiagnostics, clearOnChange, showInlineHighlights, showGutterMarkers, showActiveLine, showStatusBar, layerName | setDiagnostics, clearDiagnostics, nextDiagnostic, prevDiagnostic | | CompletionExtension | An autocomplete popup fed by providers. | providers, debounceMs, maxItems, maxVisibleItems, minPrefixLength, autoTrigger, triggerCharacters, acceptOnEnter, acceptOnTab, closeOnBlur | showCompletions, closeCompletions, nextCompletion, prevCompletion, acceptCompletion | | FormattingExtension | Format the document or the selection, with a timeout and cancellation. | formatter, timeoutMs, showStatusBar, successDurationMs, errorDurationMs | formatDocument, formatSelection | | ContextMenuExtension | A right-click menu of commands. | items, closeOnBlur, closeOnScroll | openContextMenu, closeContextMenu | | EditingCommandsExtension | Comments and line editing. | lineCommentToken, blockCommentTokens, addSpaceAfterLineComment, registerKeybindings | toggleLineComment, toggleBlockComment, duplicateLine, moveLineUp, moveLineDown, joinLines, goToLine, openGoToLine, closeGoToLine | | ActiveLineAndIndentGuidesExtension | Highlights the current line and draws indent guides. | activeLine, indentGuides, guideStepColumns, maxGuideDepth, maxGuideLines | | | BracketMatchingExtension | Highlights the matching bracket. | | | | CodeFoldingExtension | Fold bracketed blocks and markup tags. | | fold, unfold, foldAll, unfoldAll | | HoverTooltipAndCodeActionsExtension | Hover tooltips and a code-action menu. | hoverProvider, codeActionsProvider, hoverDelayMs, maxCodeActions, ... | showCodeActions, hideHoverTooltip | | createLanguageServiceExtensions(config) | One config that drives highlighting, diagnostics, completions, formatting, hover and code actions together; see below. | | refreshLanguageDiagnostics |

The editor itself always has undo, redo, insertTab and save.

Default shortcuts:

| Shortcut | Command | | --- | --- | | Cmd/Ctrl+F, Cmd/Ctrl+H | find, replace | | F3, Shift+F3, Cmd/Ctrl+G | findNext, findPrev, findNext | | Cmd/Ctrl+Z, Cmd/Ctrl+Y or Shift+Z | undo, redo | | Tab, Shift+Tab | insertTab, outdent | | Cmd/Ctrl+Shift+[ and ] | fold, unfold | | Cmd/Ctrl+Space | showCompletions | | Shift+Alt+F | formatDocument | | Cmd/Ctrl+/ , Alt+Shift+A | toggleLineComment, toggleBlockComment | | Cmd/Ctrl+Shift+D | duplicateLine | | Alt+Up, Alt+Down | moveLineUp, moveLineDown | | Cmd/Ctrl+J, Cmd/Ctrl+L | joinLines, openGoToLine | | Cmd/Ctrl+. | showCodeActions | | Cmd/Ctrl+Shift+T | toggleTheme (with ThemeExtension) |

Connect a language service

createLanguageServiceExtensions takes one config and returns the extensions that use it. Give it a function for each thing you can provide; all of them are optional, and the async ones receive an AbortSignal that fires when a newer request replaces them:

import { createEditor, createLanguageServiceExtensions, type CompletionContext } from '@editora/light-code-editor';
import '@editora/light-code-editor/light-code-editor.css';

const container = document.getElementById('editor') as HTMLElement;

const languageService = createLanguageServiceExtensions({
  languageId: 'html',
  highlight: ({ text }) => text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;'),
  diagnostics: async ({ text, abortSignal }) => {
    if (abortSignal?.aborted) {
      return [];
    }

    return text.includes('onclick=')
      ? [
          {
            severity: 'warning',
            message: 'Inline handlers need review',
            range: {
              start: { line: 0, column: 8 },
              end: { line: 0, column: 15 },
            },
          },
        ]
      : [];
  },
  completionProviders: [
    (context: CompletionContext) =>
      context.prefix.startsWith('di') ? [{ label: 'div', kind: 'tag', insertText: 'div' }] : [],
  ],
  hover: ({ diagnostics }) =>
    diagnostics[0]
      ? {
          title: diagnostics[0].code || diagnostics[0].severity,
          content: diagnostics[0].message,
          range: diagnostics[0].range,
        }
      : null,
  codeActions: ({ lineText, position }) =>
    lineText.includes('onclick=')
      ? [
          {
            label: 'Remove inline onclick handler',
            run: (editor) => {
              const start = lineText.indexOf('onclick=');
              editor.replace(
                {
                  start: { line: position.line, column: Math.max(0, start - 1) },
                  end: { line: position.line, column: start + 'onclick="save()"'.length },
                },
                '',
              );
            },
          },
        ]
      : [],
});

const editor = createEditor(container, {
  value: '<button onclick="save()">Save</button>\n<div class="note">Hello</div>\n',
  extensions: languageService.extensions,
});

editor.executeCommand('refreshLanguageDiagnostics');
editor.executeCommand('showCodeActions');

Diagnostics are { severity, message, range, source?, code? }, with severity one of error, warning, info or hint. Completion items are { label, kind, detail?, insertText? }. The result is { extensions, languageServiceExtension, syntaxHighlightingExtension, diagnosticsExtension, completionExtension, formattingExtension, hoverCodeActionsExtension }, so you can keep a handle on any of them.

FormattingExtension takes a formatter(context) that returns the new text, or { text, range, selection, cursor } when it needs to control the edit. createLightweightFormatter() is a small, dependency-free formatter for common snippets. It is conservative and no replacement for Prettier, Black or gofmt.

Decorations

Decorations tint lines, add gutter badges and mark ranges of text. They are drawn in overlay layers, so the editable text is not touched. Lines and columns start at 0.

import { createEditor, type EditorDecoration } from '@editora/light-code-editor';
import '@editora/light-code-editor/light-code-editor.css';

const editor = createEditor(document.getElementById('editor') as HTMLElement, {
  value: `const total = items.reduce((sum, item) => sum + item.price, 0);
const tax = total * 0.2;
console.log(totl + tax);
`,
  lineNumbers: true,
});

const decorations: EditorDecoration[] = [
  // Tint a whole line.
  { id: 'review-line', type: 'line', line: 2, className: 'lce-decoration-line--active' },
  // Put a badge in the gutter.
  { id: 'review-gutter', type: 'gutter', line: 2, label: '●', title: 'Unknown name', className: 'lce-decoration-gutter--error' },
  // Mark a range of text.
  {
    id: 'review-inline',
    type: 'inline',
    range: { start: { line: 2, column: 12 }, end: { line: 2, column: 16 } },
    style: {
      backgroundColor: 'rgba(255, 107, 107, 0.18)',
      textDecoration: 'underline wavy rgba(255, 107, 107, 0.95)',
    },
  },
];

// A layer is replaced as a whole: call setDecorations again with the full next set.
editor.setDecorations('review', decorations);
  • clearDecorations('review') removes one layer; clearDecorations() removes them all.
  • Inline decorations use the browser's CSS Highlight API, which keeps them fast and selection-safe. Backgrounds and underlines work best; text colour is less reliable while syntax highlighting is on.

Write your own extension

An extension has a name, a setup(editor) and a destroy():

import { createEditor, type EditorAPI, type EditorExtension } from '@editora/light-code-editor';
import '@editora/light-code-editor/light-code-editor.css';

// An extension has a name, a setup() that receives the editor, and a destroy().
class TimestampExtension implements EditorExtension {
  public readonly name = 'timestamp';

  setup(editor: EditorAPI): void {
    // Commands can be run with editor.executeCommand(), a keymap or a menu.
    editor.registerCommand('insertTimestamp', () => {
      const cursor = editor.getCursor();
      const text = `// ${new Date().toISOString()}\n`;
      editor.replace({ start: cursor.position, end: cursor.position }, text);
    });
  }

  destroy(): void {
    // Release anything you created in setup().
  }
}

const editor = createEditor(document.getElementById('editor') as HTMLElement, {
  value: 'const answer = 42;\n',
  extensions: [new TimestampExtension()],
});

editor.executeCommand('insertTimestamp');

Extensions can also use editor.getView() and editor.getConfig() to work with the rendered surface and the startup options.

Styling

Import @editora/light-code-editor/light-code-editor.css once. The editor is themed with CSS custom properties, so prefer these to the internal selectors: --editor-background, --editor-foreground, --editor-cursor, --editor-gutter-background, --editor-gutter-foreground and --editor-gutter-border. The panels that float over the editor (find, go to line, completions, menus, hover cards) read a second group: --editor-surface, --editor-surface-hover, --editor-border-strong, --editor-accent, --editor-accent-soft, --editor-accent-contrast and --editor-shadow. setTheme() writes all of them; override any after it to match your brand.

The stable hooks for styling decorations are .lce-decoration-line, .lce-decoration-gutter, .lce-decoration-line--active and .lce-decoration-gutter--error. The rendered parts carry data attributes: [data-lce-editor-container] (the outer box), [data-editora-editor] (the scroll wrapper), [data-editor-gutter] (the line numbers) and the two decoration overlays, [data-editor-line-decorations] and [data-editor-gutter-decorations].

Part of Editora

This is the code editor behind the source view of @editora/markdown-editor and the code plugins of @editora/plugins.

License

MIT © Ajay Kumar