Skip to content

Conversation

@gadenbuie
Copy link
Member

Overview

input_code_editor() is a Shiny input that provides a lightweight code editor with syntax highlighting, powered by prism-code-editor. It supports 20+ languages (a subset of those supported by prism-code-editor), multiple themes, and automatic light/dark mode switching.

File Layout

bslib/
├── R/
│   └── input-code-editor.R          # R API: input_code_editor(), update_code_editor()
├── srcts/
│   └── src/components/
│       └── codeEditor.ts            # BslibCodeEditor web component
├── inst/
│   ├── lib/
│   │   └── prism-code-editor/       # Vendored prism-code-editor library
│   ├── components/scss
│   │   └── input_code_editor.scss   # Shiny code editor styles, compiled with other bslib component styles
│   └── examples-shiny/
│       └── code-editor/app.R        # Demo application
│
└── tests/testthat/
    └── test-input-code-editor.R     # Unit tests

Architecture

Key Design Decisions

  1. Custom Element: The editor is a web component (<bslib-code-editor>) extending HTMLElement. This provides standard lifecycle hooks and attribute reflection, simplifying Shiny integration via the shared makeInputBinding() helper.

  2. Separate Bundle: code-editor.js is NOT bundled into components.min.js. It's loaded only when input_code_editor() is used, keeping the main bslib bundle small.

  3. ES Modules: The code-editor bundle uses ESM format to support dynamic imports of language grammars at runtime.

  4. Lazy Loading: Language grammars and themes are loaded on-demand when the editor initializes (connectedCallback()) or when receiving an update messages from the server, not upfront.

  5. Theme Watching: Each editor instance creates a MutationObserver that watches <html data-bs-theme> to automatically switch between light/dark themes. Theme stylesheets are shared across all instances and never unloaded.

R API (R/input-code-editor.R)

Exported Functions

Function Purpose
input_code_editor() Create a code editor input
update_code_editor() Update editor from server
code_editor_themes() List available themes

Internal Functions

Function Purpose
code_editor_dependencies() Returns all HTML dependencies
code_editor_dependency_prism() Prism library dependency
code_editor_dependency_js() bslib binding JS dependency
arg_match_language() Validate language parameter
arg_match_theme() Validate theme parameter
check_value_line_count() Warn if >1000 lines

HTML Output Structure

The component uses a custom element <bslib-code-editor> with kebab-case attributes:

<bslib-code-editor
    id="{id}"
    language="{language}"
    value="{value}"
    theme-light="{theme_light}"
    theme-dark="{theme_dark}"
    readonly="{read_only}"
    line-numbers="{line_numbers}"
    word-wrap="{word_wrap}"
    tab-size="{tab_size}"
    insert-spaces="{insert_spaces}">
  <label for="{id}">{label}</label>
  <div class="code-editor"></div>  <!-- Editor mounts here -->
</bslib-code-editor>

TypeScript Web Component (srcts/src/components/codeEditor.ts)

The editor is implemented as a custom element (<bslib-code-editor>) that extends HTMLElement and implements CustomElementInputGetValue<string> for Shiny integration.

Class: BslibCodeEditor

Static Properties:

  • tagName = "bslib-code-editor": Custom element tag name
  • isShinyInput = true: Marks this as a Shiny input for makeInputBinding()
  • observedAttributes: List of attributes that trigger attributeChangedCallback()

Static Methods (shared across all instances):

  • #getBasePath(): Locates and caches path to prism-code-editor assets
  • #loadLanguage(): Dynamically imports language grammars (cached)
  • #loadTheme(): Loads theme stylesheets (cached, never unloaded)

Instance Properties (reflect to/from HTML attributes):

  • language, readonly, lineNumbers, wordWrap, tabSize, insertSpaces
  • themeLight, themeDark
  • value: Current editor content (get/set on prismEditor)

Lifecycle Methods:

  • connectedCallback(): Initializes editor when element is added to DOM
  • disconnectedCallback(): Cleans up MutationObserver when removed
  • attributeChangedCallback(): Responds to attribute changes by updating prism-code-editor

Key Instance Methods:

  • getValue(): Returns current content (for Shiny input binding)
  • receiveMessage(): Handles update_code_editor() calls from R
  • _initializeEditor(): Creates prism-code-editor instance, sets up Ctrl+Enter and blur handlers
  • _setupThemeWatcher(): Watches data-bs-theme on <html> to switch themes
  • _handleLanguageChange(): Loads new grammar and updates editor

Shiny Integration

The Shiny input binding is created via the shared makeInputBinding() helper:

customElements.define(BslibCodeEditor.tagName, BslibCodeEditor);

if (window.Shiny) {
  makeInputBinding<string>(BslibCodeEditor.tagName);
}

The makeInputBinding() helper (from webcomponents/_makeInputBinding.ts) creates a standard Shiny input binding that:

  • Finds elements by tag name
  • Delegates getValue() and receiveMessage() to the custom element instance
  • Uses the element's onChangeCallback for value updates

tsconfig.json Note

The module: esnext setting is required for dynamic imports. The ts-node section overrides this to commonjs for the build script only.

Vendoring (tools/yarn_install.R)

How It Works

  1. inst/package.json declares prism-code-editor-full dependency (aliased from prism-code-editor-lightweight)
  2. yarn install in inst/ downloads to node_modules/
  3. node_modules/ is moved to lib/
  4. Script copies needed files from prism-code-editor-full/dist/ to prism-code-editor/
  5. Full package is deleted, only selective files remain
  6. Version is written to R/versions.R

Updating prism-code-editor

  1. Update version in inst/package.json:

    "prism-code-editor-full": "npm:prism-code-editor-lightweight@X.Y.Z"
  2. Run Rscript tools/yarn_install.R or Rscript tools/main.R

  3. Verify R/versions.R has updated version

  4. Run tests: devtools::test(filter = "code-editor")

Adding New Languages

Languages are loaded dynamically from inst/lib/prism-code-editor/prism/languages/. To add a new language:

  1. Verify the grammar file exists in prism/languages/{lang}.js
  2. Add to code_editor_bundled_languages in tools/yarn_install.R and re-run the yarn install script.
  3. Verify code_editor_bundled_languages in R/versions.R is updated.
  4. Update @param language documentation in R/input-code-editor.R.

Adding New Themes

Themes are CSS files in inst/lib/prism-code-editor/themes/. Available themes are auto-discovered by code_editor_themes().

To add a custom theme:

  1. Add {theme-name}.css to inst/lib/prism-code-editor/themes/
  2. It will automatically appear in code_editor_themes() output

Testing

Unit tests of input_code_editor() are in tests/testthat/test-input-code-editor.R and can be run with devtools::test(filter = "code-editor").

An example Shiny app demonstrating the editor is in inst/examples-shiny/code-editor/app.R and can be run with:

shiny::runApp("inst/examples-shiny/code-editor")

# For package users:
shiny::runExample("code-editor", package = "bslib")

CSS Customization

The editor uses CSS variables for Bootstrap integration. Key selectors:

  • bslib-code-editor - Custom element (outer container)
  • .code-editor - Inner editor container where prism-code-editor mounts
  • .code-editor-submit-flash - Flash animation on Ctrl+Enter

See inst/components/scss/input_code_editor.scss for full styles.

gadenbuie and others added 26 commits December 29, 2025 15:35
Each theme is wrapped with attribute
selectors that match the editor's `data-theme-light`/`data-theme-dark` attributes, combined with the page's `data-bs-theme` attribute, using CSS nesting (supported in all modern browsers since late 2023).
gadenbuie added a commit to posit-dev/py-shiny that referenced this pull request Dec 31, 2025
Ports the `input_code_editor()` component from bslib PR #1274 to py-shiny.

## Overview
`input_code_editor()` is a lightweight code editor input with syntax
highlighting powered by prism-code-editor. It supports 20+ languages,
multiple themes, and automatic light/dark mode switching.

## Added
- `input_code_editor()` - Create a code editor input
- `update_code_editor()` - Update editor from server
- `code_editor_themes()` - List available themes

## Key decisions for human review

1. **Dependency structure**: The code editor uses two HTML dependencies:
   - `prism-code-editor` (vendored library for syntax highlighting)
   - `bslib-code-editor-js` (the Shiny input binding)

2. **Themes list**: Manually hardcoded the theme list to match the
   vendored CSS files. The R version discovers these dynamically, but
   Python doesn't have easy access to the vendored directory at runtime.

3. **Language validation**: Using a hardcoded tuple of supported languages
   instead of dynamic discovery, matching the R implementation's approach.

4. **Manual vendoring**: Due to issues with the automated vendoring script
   (ionRangeSlider CSS step fails), assets were manually copied from the
   bslib feat/input-code branch. Updated htmlDependencies.R to include
   the prism-code-editor copy for future runs once bslib PR is merged.

5. **Value handling**: The `value` parameter accepts either a string or
   a sequence of strings (lines), matching Python idioms.

6. **Fill behavior**: Added `html-fill-container` and `html-fill-item`
   classes for fillable layout support, similar to other fill components.

Related: rstudio/bslib#1274

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants