Conversation
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).
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>
There was a problem hiding this comment.
Well done and nice feature! 👏
One slight concern I have is that it makes the problem in posit-dev/py-shiny#2125 slightly worse (will add ~100 new files to shiny wheel). That said, I think we could cut ~300 (about 25%) by moving Theme to a separate package, so maybe that's the more worthwhile cost saving?
There was a problem hiding this comment.
I'm tempted to say that, because this doesn't depend on Bootstrap Sass, it should get compiled separately and brought in with the code editor dependency instead.
I think this would simply things on the py-shiny end, but not sure if it's actually worth it or not -- your call
There was a problem hiding this comment.
Yeah this is how I set it up initially and I'd be okay with moving back to it. I was trying to integrate everything into the existing build process a little more, but there's not too far you can go if we want to keep the code editor dependencies separate.
I'll switch it back. We don't really gain anything by keeping them together, but separating them would untangle the code editor enough that it would help us move it into a separate package if we ever needed to. It just wasn't super clear where I'd put that .css file: I had it in inst/components/dist/ initially, but didn't like that dist/ sounds like something you can delete and have re-built. Maybe could just be a file in components/...
There was a problem hiding this comment.
I moved it into a static file that lives in srcts/src/components/codeEditor.css and gets moved into inst/components/dist during the build process.
| Sys.glob(file.path(src, "*.css")) | ||
| ) | ||
| core_files <- core_files[!grepl(theme_js_pattern, basename(core_files))] | ||
| file.copy(core_files, dest) |
There was a problem hiding this comment.
I feel like it's maybe worth removing some of the CSS/JS that isn't needed? Here's a handful that Claude identified:
- autocomplete*.css, code-block.css, folding.css, guides.css
- extensions/cursor.js, extensions/guides.js, extensions/matchTags.js
There was a problem hiding this comment.
Agreed we could probably cut more of these. I was thinking we might want to turn some of these on or enable them behind flags, which gets harder to do when the files aren't around.
I'll take a pass at slimming this down tomorrow in a way that would be easy for us to unroll later if we want to add extensions.
There was a problem hiding this comment.
I went through and aggressively removed extensions and files we don't need. There might be one or two that we don't fully use, but it's significantly slimmer now.
While looking into this, I learned about a built-in method for adding tooltips to the editor, so I updated the editor to give decent visual feedback when you try to modify a read-only editor
3f83623 to
f338000
Compare
|
|
||
| build.onEnd(() => { | ||
| copyFileSync(source, dest); | ||
| console.log("√ -", "code-editor.css", "-", new Date().toJSON()); |
There was a problem hiding this comment.
I think you meant to log source or dest here?
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
Architecture
Key Design Decisions
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 sharedmakeInputBinding()helper.Separate Bundle:
code-editor.jsis NOT bundled intocomponents.min.js. It's loaded only wheninput_code_editor()is used, keeping the main bslib bundle small.ES Modules: The code-editor bundle uses ESM format to support dynamic imports of language grammars at runtime.
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.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
input_code_editor()update_code_editor()code_editor_themes()Internal Functions
code_editor_dependencies()code_editor_dependency_prism()code_editor_dependency_js()arg_match_language()arg_match_theme()check_value_line_count()HTML Output Structure
The component uses a custom element
<bslib-code-editor>with kebab-case attributes:TypeScript Web Component (
srcts/src/components/codeEditor.ts)The editor is implemented as a custom element (
<bslib-code-editor>) that extendsHTMLElementand implementsCustomElementInputGetValue<string>for Shiny integration.Class:
BslibCodeEditorStatic Properties:
tagName = "bslib-code-editor": Custom element tag nameisShinyInput = true: Marks this as a Shiny input formakeInputBinding()observedAttributes: List of attributes that triggerattributeChangedCallback()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,insertSpacesthemeLight,themeDarkvalue: Current editor content (get/set onprismEditor)Lifecycle Methods:
connectedCallback(): Initializes editor when element is added to DOMdisconnectedCallback(): Cleans up MutationObserver when removedattributeChangedCallback(): Responds to attribute changes by updating prism-code-editorKey Instance Methods:
getValue(): Returns current content (for Shiny input binding)receiveMessage(): Handlesupdate_code_editor()calls from R_initializeEditor(): Creates prism-code-editor instance, sets up Ctrl+Enter and blur handlers_setupThemeWatcher(): Watchesdata-bs-themeon<html>to switch themes_handleLanguageChange(): Loads new grammar and updates editorShiny Integration
The Shiny input binding is created via the shared
makeInputBinding()helper:The
makeInputBinding()helper (fromwebcomponents/_makeInputBinding.ts) creates a standard Shiny input binding that:getValue()andreceiveMessage()to the custom element instanceonChangeCallbackfor value updatestsconfig.json Note
The
module: esnextsetting is required for dynamic imports. Thets-nodesection overrides this tocommonjsfor the build script only.Vendoring (
tools/yarn_install.R)How It Works
inst/package.jsondeclaresprism-code-editor-fulldependency (aliased fromprism-code-editor-lightweight)yarn installininst/downloads tonode_modules/node_modules/is moved tolib/prism-code-editor-full/dist/toprism-code-editor/R/versions.RUpdating prism-code-editor
Update version in
inst/package.json:Run
Rscript tools/yarn_install.RorRscript tools/main.RVerify
R/versions.Rhas updated versionRun 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:prism/languages/{lang}.jscode_editor_bundled_languagesintools/yarn_install.Rand re-run the yarn install script.code_editor_bundled_languagesinR/versions.Ris updated.@param languagedocumentation inR/input-code-editor.R.Adding New Themes
Themes are CSS files in
inst/lib/prism-code-editor/themes/. Available themes are auto-discovered bycode_editor_themes().To add a custom theme:
{theme-name}.csstoinst/lib/prism-code-editor/themes/code_editor_themes()outputTesting
Unit tests of
input_code_editor()are intests/testthat/test-input-code-editor.Rand can be run withdevtools::test(filter = "code-editor").An example Shiny app demonstrating the editor is in
inst/examples-shiny/code-editor/app.Rand can be run with: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+EnterSee
srcts/src/components/codeEditor.cssfor full styles.