Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
4d54e69
chore: initial plan
gadenbuie Oct 15, 2025
29f3527
chore: remove future work from plan
gadenbuie Oct 15, 2025
c39c6a9
chore: add dev advice to plan
gadenbuie Oct 15, 2025
1193f40
feat: set up npm workflow for prism-code-editor dependencies
gadenbuie Oct 15, 2025
a98a643
feat: create JavaScript binding and base styling for code editor
gadenbuie Oct 15, 2025
390d7dd
feat: implement R package integration for code editor
gadenbuie Oct 15, 2025
22abded
feat: add example Shiny app and comprehensive tests for code editor
gadenbuie Oct 15, 2025
6256a55
chore: Copy all the prism-code-editor assets
gadenbuie Oct 15, 2025
3cba808
refactor: separate prism-code-editor from shiny-input-code-editor dep
gadenbuie Oct 15, 2025
3934a41
refactor: improve base path discovery for prism-code-editor
gadenbuie Oct 15, 2025
a027b11
fix: getPrismCodeEditorBasePath()
gadenbuie Oct 15, 2025
dcd1ac3
fix: copy prism code editor utils
gadenbuie Oct 15, 2025
65614d2
fix: correct language grammar import path
gadenbuie Oct 15, 2025
69bce2d
chore: include `prism/` in core deps
gadenbuie Oct 15, 2025
13038cc
chore: tweak demo app
gadenbuie Oct 15, 2025
0912c32
chore: make code editor input container a fill carrier
gadenbuie Oct 15, 2025
3e3109a
chore: use standard js events
gadenbuie Oct 15, 2025
1d96328
refactor: rename .code-editor-input to .shiny-input-code-editor
gadenbuie Oct 15, 2025
b5f9b37
fix: Consult data-theme-dark and data-theme-light when changing themes
gadenbuie Oct 15, 2025
f00a471
small tweaks
gadenbuie Oct 15, 2025
beea880
chore: Add `label` and `fill`, rename `code` -> `value`, remove `plac…
gadenbuie Oct 15, 2025
733bf3d
chore: Updating code value triggers reactivity, throttle instead of d…
gadenbuie Oct 15, 2025
e3ce721
refactor: Use prism-code-editor keyCommandMap for cmd/ctrl enter beha…
gadenbuie Oct 15, 2025
ce54995
refactor: Use bs rgb css vars
gadenbuie Oct 15, 2025
f06b2b7
docs: Restructure, re-org, streamline, etc.
gadenbuie Oct 15, 2025
4af791a
feat: Add `querychat_ui_code()`
gadenbuie Oct 15, 2025
269ed31
chore: remove large context file
gadenbuie Oct 15, 2025
8e8af71
chore: warn if sending 750+ code lines
gadenbuie Oct 15, 2025
3ad2ba0
fix: r cmd check issues
gadenbuie Oct 15, 2025
a357d6d
fix: npm copy-extensions correctly copies copyButton now
gadenbuie Oct 15, 2025
0651435
chore: use rlang::warn() and not cli::cli_warn()
gadenbuie Oct 15, 2025
759d8ca
chore: update test
gadenbuie Oct 15, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -263,3 +263,5 @@ renv.lock

# Claude
.claude/settings.local.json
node_modules/
package-lock.json
19 changes: 19 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"name": "querychat-dependencies",
"version": "1.0.0",
"private": true,
"description": "JavaScript dependencies for querychat code editor",
"scripts": {
"install-deps": "npm install --no-save prism-code-editor cpy-cli",
"copy-deps": "npm run copy-core && npm run copy-languages && npm run copy-extensions && npm run copy-themes",
"copy-core": "cpy 'node_modules/prism-code-editor/dist/*.js' pkg-r/inst/js/prism-code-editor/ && cpy 'node_modules/prism-code-editor/dist/*.css' pkg-r/inst/js/prism-code-editor/ && cpy 'node_modules/prism-code-editor/dist/utils/*.js' pkg-r/inst/js/prism-code-editor/utils/ && cpy 'node_modules/prism-code-editor/dist/prism/**/*.js' pkg-r/inst/js/prism-code-editor/prism/",
"copy-languages": "cpy 'node_modules/prism-code-editor/dist/languages/*.js' pkg-r/inst/js/prism-code-editor/languages/",
"copy-extensions": "cpy 'node_modules/prism-code-editor/dist/extensions/copyButton/*' pkg-r/inst/js/prism-code-editor/extensions/copyButton/ && cpy 'node_modules/prism-code-editor/dist/extensions/*' pkg-r/inst/js/prism-code-editor/extensions/",
"copy-themes": "cpy 'node_modules/prism-code-editor/dist/themes/*.css' pkg-r/inst/js/prism-code-editor/themes/",
"update-deps": "npm run install-deps && npm run copy-deps"
},
"devDependencies": {
"prism-code-editor": "^3.0.0",
"cpy-cli": "^6.0.0"
}
}
4 changes: 4 additions & 0 deletions pkg-r/NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,20 @@ S3method(querychat_data_source,DBIConnection)
S3method(querychat_data_source,data.frame)
S3method(test_query,dbi_source)
export(cleanup_source)
export(code_editor_themes)
export(create_system_prompt)
export(execute_query)
export(get_db_type)
export(get_schema)
export(input_code_editor)
export(querychat_app)
export(querychat_data_source)
export(querychat_greeting)
export(querychat_init)
export(querychat_server)
export(querychat_sidebar)
export(querychat_ui)
export(querychat_ui_code)
export(test_query)
export(update_code_editor)
importFrom(lifecycle,deprecated)
319 changes: 319 additions & 0 deletions pkg-r/R/input_code_editor.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,319 @@
#' Code editor input for Shiny
#'
#' Creates an interactive code editor input that can be used in Shiny
#' applications. The editor provides syntax highlighting, line numbers, and
#' other code editing features powered by Prism Code Editor.
#'
#' @section Keyboard shortcuts:
#' The editor supports the following keyboard shortcuts:
#' - `Ctrl/Cmd+Enter`: Submit the current code to R
#' - `Ctrl/Cmd+Z`: Undo
#' - `Ctrl/Cmd+Shift+Z`: Redo
#' - `Tab`: Indent selection
#' - `Shift+Tab`: Dedent selection
#'
#' @section Update triggers:
#' The editor value is sent to R when:
#' - The editor loses focus (blur event)
#' - The user presses `Ctrl/Cmd+Enter`
#'
#' @section Theme switching:
#' The editor automatically switches between `theme_light` and `theme_dark`
#' when used with [bslib::input_dark_mode()].
#'
#' @examples
#' \dontrun{
#' library(shiny)
#' library(querychat)
#'
#' ui <- fluidPage(
#' input_code_editor(
#' "sql_query",
#' value = "SELECT * FROM table",
#' language = "sql"
#' )
#' )
#'
#' server <- function(input, output, session) {
#' observe({
#' print(input$sql_query)
#' })
#' }
#'
#' shinyApp(ui, server)
#' }
#'
#' @param id Input ID. Access the current value with `input$<id>`.
#' @param value Initial code content. Default is an empty string.
#' @param label Display label for the input. Default is `NULL` for no label.
#' @param ... Must be empty. Prevents accidentally passing unnamed arguments.
#' @param language Programming language for syntax highlighting. Must be one of:
#' `"sql"`, `"python"`, `"r"`, `"javascript"`, `"html"`, `"css"`, `"json"`,
#' `"bash"`, `"markdown"`, `"yaml"`, `"xml"`. Default is `"sql"`.
#' @param height CSS height of the editor. Default is `"300px"`.
#' @param width CSS width of the editor. Default is `"100%"`.
#' @param theme_light Theme to use in light mode. See [code_editor_themes()] for
#' available themes. Default is `"github-light"`.
#' @param theme_dark Theme to use in dark mode. See [code_editor_themes()] for
#' available themes. Default is `"github-dark"`.
#' @param read_only Whether the editor should be read-only. Default is `FALSE`.
#' @param line_numbers Whether to show line numbers. Default is `TRUE`.
#' @param word_wrap Whether to wrap long lines. Default is `FALSE`.
#' @param tab_size Number of spaces per tab. Default is `2`.
#' @param indentation Type of indentation: `"space"` or `"tab"`. Default is
#' `"space"`.
#' @inheritParams bslib::card
#' @param session Shiny session object, for expert use only.
#'
#' @return An HTML tag object that can be included in a Shiny UI.
#'
#' @describeIn input_code_editor Create a light-weight code editor input
#' @export
input_code_editor <- function(
id,
value = "",
label = NULL,
...,
language = "sql",
height = "auto",
width = "100%",
theme_light = "github-light",
theme_dark = "github-dark",
read_only = FALSE,
line_numbers = TRUE,
word_wrap = FALSE,
tab_size = 2,
indentation = c("space", "tab"),
fill = TRUE
) {
# Ensure no extra arguments
rlang::check_dots_empty()
stopifnot(rlang::is_bool(fill))

check_value_line_count(value)

# Validate inputs
language <- arg_match_language(language)
theme_light <- arg_match_theme(theme_light, "theme_light")
theme_dark <- arg_match_theme(theme_dark, "theme_dark")

indentation <- rlang::arg_match(indentation)
insert_spaces <- (indentation == "space")

# Create inner container that will hold the actual editor
editor_inner <- htmltools::tags$div(
class = "code-editor",
bslib::as_fill_item(),
style = htmltools::css(
display = "grid"
)
)

label_tag <- asNamespace("shiny")[["shinyInputLabel"]](id, label)

htmltools::tags$div(
id = id,
class = "shiny-input-code-editor",
style = htmltools::css(
height = height,
width = width
),
if (fill) bslib::as_fill_item(),
bslib::as_fillable_container(),
`data-language` = language,
`data-initial-code` = value,
`data-theme-light` = theme_light,
`data-theme-dark` = theme_dark,
`data-read-only` = tolower(as.character(read_only)),
`data-line-numbers` = tolower(as.character(line_numbers)),
`data-word-wrap` = tolower(as.character(word_wrap)),
`data-tab-size` = as.character(tab_size),
`data-insert-spaces` = tolower(as.character(insert_spaces)),
label_tag,
editor_inner,
html_dependency_code_editor(),
)
}

#' @describeIn input_code_editor Update the code editor input value and settings
#' @export
update_code_editor <- function(
id,
value = NULL,
...,
language = NULL,
theme_light = NULL,
theme_dark = NULL,
read_only = NULL,
line_numbers = NULL,
word_wrap = NULL,
tab_size = NULL,
indentation = NULL,
session = shiny::getDefaultReactiveDomain()
) {
# Ensure no extra arguments
rlang::check_dots_empty()

# Validate inputs if provided
if (!is.null(language)) {
language <- arg_match_language(language, "language")
}
if (!is.null(theme_light)) {
theme_light <- arg_match_theme(theme_light, "theme_light")
}
if (!is.null(theme_dark)) {
theme_dark <- arg_match_theme(theme_dark, "theme_dark")
}

# Build message with only non-NULL values
message <- list()

if (!is.null(value)) {
check_value_line_count(value)
message$code <- value
}
if (!is.null(language)) {
message$language <- language
}
if (!is.null(theme_light)) {
message$theme_light <- theme_light
}
if (!is.null(theme_dark)) {
message$theme_dark <- theme_dark
}
if (!is.null(read_only)) {
message$read_only <- read_only
}
if (!is.null(line_numbers)) {
message$line_numbers <- line_numbers
}
if (!is.null(word_wrap)) {
message$word_wrap <- word_wrap
}
if (!is.null(tab_size)) {
message$tab_size <- tab_size
}
if (!is.null(indentation)) {
indentation <- rlang::arg_match(indentation, c("space", "tab"))
message$indentation <- indentation
}

# Send message to JavaScript binding
session$sendInputMessage(id, message)

invisible(NULL)
}

#' HTML dependency for the code editor component
#'
#' This function returns an htmlDependency object that bundles all necessary
#' JavaScript and CSS files for the Prism Code Editor component.
#'
#' @return An htmlDependency object
#' @keywords internal
html_dependency_code_editor <- function() {
dep_code_editor <- htmltools::htmlDependency(
name = "shiny-input-code-editor",
version = utils::packageVersion("querychat"),
package = "querychat",
src = "js",
script = "code-editor-binding.js",
stylesheet = "code-editor.css",
all_files = FALSE
)

htmltools::tagList(
html_dependency_prism_code_editor(),
dep_code_editor
)
}

html_dependency_prism_code_editor <- function() {
htmltools::htmlDependency(
name = "prism-code-editor",
version = "3.0.0",
package = "querychat",
src = "js/prism-code-editor",
script = list(src = "index.js", type = "module"),
stylesheet = c("layout.css", "copy.css"),
all_files = TRUE
)
}

#' @describeIn input_code_editor List available code editor syntax highlighting
#' themes
#' @export
code_editor_themes <- function() {
themes_dir <- system.file(
"js/prism-code-editor/themes",
package = "querychat"
)

if (!dir.exists(themes_dir)) {
return(character(0))
}

theme_files <- list.files(themes_dir, pattern = "\\.css$")
sub("\\.css$", "", theme_files)
}

arg_match_theme <- function(theme, arg_name = "theme") {
if (is.null(theme)) {
return(invisible(NULL))
}

available_themes <- code_editor_themes()

rlang::arg_match(
theme,
values = available_themes,
error_arg = arg_name,
error_call = rlang::caller_env()
)
}

arg_match_language <- function(language, arg_name = "language") {
if (is.null(language)) {
return(invisible(NULL))
}

# List of initially supported languages - these match the grammar files
# we've bundled from prism-code-editor
supported_languages <- c(
"sql",
"python",
"r",
"javascript",
"html",
"css",
"json",
"bash",
"markdown",
"yaml",
"xml"
)

rlang::arg_match(
language,
values = supported_languages,
error_arg = arg_name,
error_call = rlang::caller_env()
)
}

check_value_line_count <- function(value) {
if (is.null(value) || !is.character(value) || length(value) == 0) {
return(invisible(NULL))
}

line_count <- length(strsplit(value, "\n", fixed = TRUE)[[1]])

if (line_count >= 750) {
rlang::warn(c(
sprintf("Code editor value contains %d lines.", line_count),
"i" = "The editor may experience performance issues with 750 or more lines."
))
}

invisible(NULL)
}
Loading