Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [UNRELEASED]

### New features

* Added a new `ui.input_code_editor()` element that allows for light-weight code editing with syntax highlighting, using the [prism-code-editor](https://prism-code-editor.netlify.app/) library. The editor supports 20+ languages, more than a dozen themes, and automatic light/dark mode switching. (#2128)

## [1.5.1] - 2025-12-08

Expand Down
2 changes: 2 additions & 0 deletions docs/_quartodoc-core.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ quartodoc:
- ui.input_text
- ui.input_text_area
- ui.input_submit_textarea
- ui.input_code_editor
- ui.input_password
- ui.input_action_button
- ui.input_action_link
Expand Down Expand Up @@ -168,6 +169,7 @@ quartodoc:
- name: ui.update_text_area
dynamic: "shiny.ui.update_text"
- ui.update_submit_textarea
- ui.update_code_editor
- ui.update_navset
- ui.update_action_button
- ui.update_action_link
Expand Down
2 changes: 2 additions & 0 deletions docs/_quartodoc-express.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ quartodoc:
- express.ui.input_text
- express.ui.input_text_area
- express.ui.input_submit_textarea
- express.ui.input_code_editor
- express.ui.input_password
- express.ui.input_action_button
- express.ui.input_action_link
Expand Down Expand Up @@ -128,6 +129,7 @@ quartodoc:
- express.ui.update_text
- express.ui.update_text_area
- express.ui.update_submit_textarea
- express.ui.update_code_editor
- express.ui.update_navset
- express.ui.update_action_button
- express.ui.update_action_link
Expand Down
1 change: 1 addition & 0 deletions docs/_quartodoc-testing.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ quartodoc:
- playwright.controller.InputBookmarkButton
- playwright.controller.InputCheckbox
- playwright.controller.InputCheckboxGroup
- playwright.controller.InputCodeEditor
- playwright.controller.InputDarkMode
- playwright.controller.InputDate
- playwright.controller.InputDateRange
Expand Down
244 changes: 244 additions & 0 deletions examples/code-editor/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
from pathlib import Path
from typing import get_args

from shiny import App, Inputs, Outputs, Session, reactive, render, ui
from shiny.ui._input_code_editor import SUPPORTED_LANGUAGES as languages
from shiny.ui._input_code_editor_bundle import CodeEditorTheme

# Load sample code snippets into a dictionary
examples_dir = Path(__file__).parent / "examples"
sample_code: dict[str, str] = {}

for file_path in examples_dir.iterdir():
if file_path.is_file():
language = file_path.stem
sample_code[language] = file_path.read_text()


# Available themes
themes = list(get_args(CodeEditorTheme))

app_ui = ui.page_sidebar(
ui.sidebar(
ui.input_text("label", "Label:", value="Code Editor"),
ui.input_select(
"language",
"Language:",
choices=languages,
selected="python",
),
ui.input_action_button(
"load_sample",
"Load Sample Code",
class_="btn-secondary btn-sm w-100 mb-2",
),
ui.input_action_button(
"clear_code",
"Clear Editor",
class_="btn-warning btn-sm w-100 mb-2",
),
ui.input_select(
"theme_light",
"Light Theme:",
choices=themes,
selected="github-light",
),
ui.input_select(
"theme_dark",
"Dark Theme:",
choices=themes,
selected="github-dark",
),
ui.p(
ui.input_dark_mode(id="dark_mode", mode="light"),
" ",
ui.input_action_link(
"toggle_dark_mode",
"Toggle Theme",
),
class_="text-end small",
),
ui.input_checkbox("read_only", "Read Only", value=False),
ui.input_checkbox("line_numbers", "Line Numbers", value=True),
ui.input_checkbox("word_wrap", "Word Wrap", value=False),
ui.input_slider("tab_size", "Tab Size:", min=2, max=8, value=4, step=1),
ui.input_radio_buttons(
"indentation",
"Indentation:",
choices={"space": "Spaces", "tab": "Tabs"},
selected="space",
inline=True,
),
width=300,
title="Editor Controls",
gap="0.5rem",
),
ui.layout_columns(
ui.card(
ui.card_header("Code Editor"),
ui.card_body(
ui.p(
"This editor supports syntax highlighting, line numbers, word wrap, and more. ",
"Try pressing ",
ui.tags.kbd("Ctrl/Cmd+Enter"),
" to submit the code.",
),
ui.input_code_editor(
"code",
value="import pandas as pd\n\n# Sample Python code\ndata = pd.DataFrame({\n 'x': [1, 2, 3, 4, 5],\n 'y': [2, 4, 6, 8, 10]\n})\n\nprint(data.describe())",
label="Code Editor",
language="python",
line_numbers=True,
word_wrap=False,
fill=True,
),
),
),
ui.layout_columns(
ui.navset_card_underline(
ui.nav_panel("Value", ui.output_text_verbatim("code_output")),
ui.nav_panel("Settings", ui.output_text_verbatim("editor_info")),
title="Editor Info",
),
ui.card(
ui.card_header("Features & Keyboard Shortcuts"),
ui.card_body(
ui.tags.ul(
ui.tags.li(
ui.tags.kbd("Ctrl/Cmd+Enter"),
" - Submit code (triggers reactive update)",
),
ui.tags.li(ui.tags.kbd("Ctrl/Cmd+Z"), " - Undo"),
ui.tags.li(ui.tags.kbd("Ctrl/Cmd+Shift+Z"), " - Redo"),
ui.tags.li(ui.tags.kbd("Tab"), " - Indent selection"),
ui.tags.li(ui.tags.kbd("Shift+Tab"), " - Dedent selection"),
ui.tags.li("Copy button in top-right corner"),
ui.tags.li("Automatic light/dark theme switching"),
ui.tags.li("Update on blur (when editor loses focus)"),
),
),
),
col_widths=12,
),
),
title="Code Editor Demo",
class_="bslib-page-dashboard",
)


def server(input: Inputs, output: Outputs, session: Session):
@reactive.effect
@reactive.event(input.label)
def _():
ui.update_code_editor("code", label=input.label())

@reactive.effect
@reactive.event(input.language)
def _():
language = input.language()
# Resolve aliases for checking sample availability
resolved_lang = {
"plain": "markdown",
"html": "markup",
}.get(language, language)

# Update button disabled state based on sample availability
has_sample = resolved_lang in sample_code
ui.update_action_button(
"load_sample",
disabled=not has_sample,
)
ui.update_code_editor("code", language=language)

@reactive.effect
@reactive.event(input.theme_light)
def _():
ui.update_code_editor("code", theme_light=input.theme_light())

@reactive.effect
@reactive.event(input.theme_dark)
def _():
ui.update_code_editor("code", theme_dark=input.theme_dark())

@reactive.effect
@reactive.event(input.read_only)
def _():
ui.update_code_editor("code", read_only=input.read_only())

@reactive.effect
@reactive.event(input.line_numbers)
def _():
ui.update_code_editor("code", line_numbers=input.line_numbers())

@reactive.effect
@reactive.event(input.word_wrap)
def _():
ui.update_code_editor("code", word_wrap=input.word_wrap())

@reactive.effect
@reactive.event(input.tab_size)
def _():
ui.update_code_editor("code", tab_size=input.tab_size())

@reactive.effect
@reactive.event(input.indentation)
def _():
ui.update_code_editor("code", indentation=input.indentation())

@reactive.effect
@reactive.event(input.load_sample)
def _():
language = input.language()
# Resolve aliases
resolved_lang = {
"plain": "markdown",
"html": "markup",
}.get(language, language)

sample = sample_code.get(resolved_lang)
if sample is not None:
ui.update_code_editor("code", value=sample, language=language)

@reactive.effect
@reactive.event(input.clear_code)
def _():
ui.update_code_editor("code", value="")

@reactive.effect
@reactive.event(input.toggle_dark_mode)
def _():
current_mode = input.dark_mode()
new_mode = "dark" if current_mode == "light" else "light"
ui.update_dark_mode(new_mode)

@render.text
def code_output():
code = input.code()
if code is None or code == "":
return "[Editor is empty]"
return code

@render.text
def editor_info():
code = input.code()
if code is None:
code = ""

lines = len(code.split("\n"))
chars = len(code)

return "\n".join(
[
f"Language: {input.language()}",
f"Lines: {lines}",
f"Characters: {chars}",
f"Read Only: {input.read_only()}",
f"Line Numbers: {input.line_numbers()}",
f"Word Wrap: {input.word_wrap()}",
f"Tab Size: {input.tab_size()}",
f"Indentation: {input.indentation()}",
]
)


app = App(app_ui, server)
33 changes: 33 additions & 0 deletions examples/code-editor/examples/bash.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
#!/bin/bash

# Script to process log files and generate a summary report
# Author: Data Team

OUTPUT_DIR="${1:-.}"
LOG_FILES=( /var/log/*.log )

# Function to count error occurrences
count_errors() {
local file="$1"
grep -c "ERROR" "$file" 2>/dev/null || echo 0
}

# Function to extract timestamp
get_timestamp() {
date "+%Y-%m-%d %H:%M:%S"
}

# Main processing loop
echo "Report generated on $(get_timestamp)" > "$OUTPUT_DIR/report.txt"

for logfile in "${LOG_FILES[@]}"; do
if [[ -f "$logfile" ]]; then
error_count=$(count_errors "$logfile")
if (( error_count > 0 )); then
echo "Processing $logfile: $error_count errors found"
tail -n 10 "$logfile" | grep "ERROR" >> "$OUTPUT_DIR/errors.log"
fi
fi
done

echo "Report complete. Results saved to $OUTPUT_DIR"
33 changes: 33 additions & 0 deletions examples/code-editor/examples/cpp.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
#include <iostream>
#include <vector>
#include <algorithm>
#include <memory>

namespace DataProcessing {
// Template function for processing collections
template <typename T>
class DataProcessor {
private:
std::vector<T> data;

public:
DataProcessor(const std::vector<T>& input) : data(input) {}

// Process data with lambda and range-based for
auto transform(auto&& func) const {
std::vector<decltype(func(data[0]))> result;
for (const auto& item : data) {
result.push_back(func(item));
}
return result;
}

// Use auto, references, and modern syntax
auto average() const -> double {
if (data.empty()) return 0.0;
double sum = 0;
for (const auto& val : data) sum += val;
return sum / data.size();
}
};
}
Loading
Loading