Skip to content

Shared read-only CodeMirror viewer for structured and large text #213

Description

@BorisTyshkevich

Reconciled 2026-07-13: implemented in PR #215; cell-detail consumption remains tracked separately in #214.

Problem

The application already bundles CodeMirror 6 for the editable SQL editor, but read-only text surfaces still use plain <pre> elements.

The cell-detail drawer is the immediate consumer. Large JSON, SQL, HTML/XML source, Markdown source, logs, and stack traces need:

  • line numbers;
  • search;
  • syntax highlighting where a language is known;
  • optional line wrapping;
  • efficient scrolling for large values;
  • reliable teardown when the drawer closes.

Reusing the editable SQL EditorPort is not appropriate. That adapter also owns:

  • per-tab undo history;
  • SQL completions;
  • hover documentation;
  • schema-aware column loading;
  • drag-and-drop insertion;
  • app-state synchronization;
  • editable keyboard behavior.

A read-only viewer needs a smaller, independent seam while sharing the common CodeMirror styling and base extensions.

Goal

Add a reusable, injected read-only CodeMirror viewer that can display source text in any document realm, including detached Data views.

Initial language modes:

  • plain text;
  • JSON;
  • SQL;
  • HTML/XML source;
  • Markdown source as plain text.

YAML is explicitly out of scope.

Architecture

New viewer seam

Add:

src/editor/code-viewer.js

Export a factory such as:

createCodeViewer({
  parent,
  document,
  text,
  language,
  wrap,
})

Return:

{
  setText(text),
  setLanguage(language),
  setWrap(enabled),
  focus(),
  destroy(),
}

Equivalent naming is acceptable.

The viewer is read-only:

EditorState.readOnly.of(true)
EditorView.editable.of(false)

It must not expose editing commands, completion, history, schema loading, drag/drop insertion, or app-state subscriptions.

Injection

Follow the existing third-party seam rule.

Expose the real factory through application construction:

const app = createApp({
  Editor,
  CodeViewer,
  // ...
});

Store it as:

app.CodeViewer

Tests for consumers may inject a lightweight stub. The real adapter receives direct unit coverage.

Do not import the CodeMirror viewer directly from results.js.

Shared CodeMirror base

Extract the genuinely common CodeMirror presentation pieces from the SQL adapter into a small module, for example:

src/editor/codemirror-base.js

Candidates:

  • common HighlightStyle mapping to existing .sql-* token classes;
  • read-only-safe search keymap;
  • base visual extensions;
  • line-number and selection styling;
  • line-wrapping compartment helper.

Do not move SQL-specific behavior into the common module:

  • SQL dialect construction;
  • completion source;
  • hover source;
  • input guards;
  • tab-state parking;
  • schema prefetch;
  • editor write-back.

The editable SQL editor must continue using the same visual token classes after the extraction.

Language registry

Use a small explicit registry:

languageExtension('text')     // []
languageExtension('json')     // json()
languageExtension('sql')      // existing SQL language support
languageExtension('xml')      // xml()
languageExtension('html')     // XML-style source highlighting in v1
languageExtension('markdown') // plain text in v1

Add only:

@codemirror/lang-json
@codemirror/lang-xml

as new runtime packages.

Do not add a second Markdown parser or Markdown language package in this issue. Rendered Markdown is handled by the existing safe Markdown parser/renderer; source mode can remain plain text.

Record the bundle-size change in the PR.

Viewer behavior

Read-only

The document cannot be changed through:

  • typing;
  • paste;
  • cut;
  • drag/drop;
  • input methods;
  • editor commands.

Selection and copying remain available.

Search

Use the existing CodeMirror search extension and keymap.

Mod-f opens local viewer search. Search UI and matches stay inside the viewer.

Line wrapping

The consumer controls wrapping:

viewer.setWrap(true | false)

Changing wrapping must not reconstruct the viewer or lose search state unnecessarily.

Large values

The viewer must receive the complete cell text.

Do not pre-truncate source content inside the viewer. Existing drawer sizing and browser memory limits remain the governing constraints.

Document realm

The cell-detail drawer can be opened:

  • in the main application document;
  • inside a detached browser tab/document.

Construct the EditorView with the correct parent/root/document so all DOM nodes belong to the target document.

Do not read the ambient global document when a target document was supplied.

Lifecycle

destroy() must:

  • destroy the EditorView;
  • release observers and event listeners;
  • be idempotent.

A drawer mode switch must destroy the outgoing viewer before replacing its DOM.

Closing the drawer must destroy the active viewer before removing the backdrop.

Tests

Real adapter

Add:

tests/unit/code-viewer.test.js

Verify:

  • mounts with the supplied text;
  • is read-only and non-editable;
  • selection/copy remains possible;
  • JSON mode loads without throwing;
  • SQL mode loads without throwing;
  • XML and HTML modes load without throwing;
  • text and Markdown modes use plain text;
  • wrapping can be toggled;
  • text can be replaced programmatically;
  • language can be reconfigured;
  • search extension is installed;
  • destroy() is idempotent;
  • a custom detached document owns the viewer DOM.

Shared-base regression

Update the SQL adapter tests to verify:

  • editable SQL behavior is unchanged;
  • completions, hover, drag/drop, undo, and tab-state parking remain intact;
  • token classes remain the same;
  • no viewer-only read-only setting leaks into the editor.

Consumer seam

Add a stub-friendly contract test showing that a consumer can:

  1. create a viewer;
  2. switch wrapping;
  3. destroy it during mode changes;
  4. destroy it again during parent teardown safely.

Files

Expected changes:

  • src/editor/codemirror-base.js
  • src/editor/code-viewer.js
  • src/editor/codemirror-adapter.js
  • src/ui/app.js or the application bootstrap injection point
  • src/main.js
  • tests/unit/code-viewer.test.js
  • tests/unit/codemirror-adapter.test.js
  • tests/unit/app.test.js
  • package.json
  • lockfile
  • E2E import maps whose raw ESM graphs reach the new packages

Acceptance criteria

  • A reusable injected read-only CodeMirror viewer exists.
  • The viewer supports text, JSON, SQL, HTML/XML source, and plain Markdown source modes.
  • JSON and XML support add only the required CodeMirror language packages.
  • YAML support is not added.
  • The viewer provides line numbers, search, selection/copy, and configurable wrapping.
  • The viewer cannot be edited through browser input paths.
  • It works in both the main and detached document realms.
  • Viewer teardown is explicit and idempotent.
  • Common CodeMirror styling is shared without moving SQL-specific editor behavior.
  • The editable SQL editor has no behavioral regression.
  • Bundle impact is documented.
  • Coverage gates and build pass.

Non-goals

  • Replacing the editable SQL EditorPort.
  • Editable JSON/XML/Markdown cells.
  • JSON parsing or formatting policy.
  • Rendered Markdown or HTML.
  • YAML.
  • CSV/TSV table parsing.
  • Automatic content-type detection.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions