diff --git a/.github/workflows/R-CMD-check.yaml b/.github/workflows/R-CMD-check.yaml new file mode 100644 index 0000000..b3e1143 --- /dev/null +++ b/.github/workflows/R-CMD-check.yaml @@ -0,0 +1,53 @@ +# Workflow derived from https://github.com/r-lib/actions/tree/v2/examples +# Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help +on: + push: + branches: [main, master] + pull_request: + +name: R-CMD-check.yaml + +permissions: read-all + +jobs: + R-CMD-check: + runs-on: ${{ matrix.config.os }} + + name: ${{ matrix.config.os }} (${{ matrix.config.r }}) + + strategy: + fail-fast: false + matrix: + config: + - {os: macos-latest, r: 'release'} + - {os: windows-latest, r: 'release'} + - {os: ubuntu-latest, r: 'devel', http-user-agent: 'release'} + - {os: ubuntu-latest, r: 'release'} + - {os: ubuntu-latest, r: 'oldrel-1'} + + env: + GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} + R_KEEP_PKG_SOURCE: yes + + steps: + - uses: actions/checkout@v4 + + - uses: r-lib/actions/setup-pandoc@v2 + + - uses: r-lib/actions/setup-r@v2 + with: + r-version: ${{ matrix.config.r }} + http-user-agent: ${{ matrix.config.http-user-agent }} + use-public-rspm: true + + - uses: r-lib/actions/setup-r-dependencies@v2 + with: + working-directory: hera + extra-packages: any::rcmdcheck + needs: check + + - uses: r-lib/actions/check-r-package@v2 + with: + working-directory: hera + upload-snapshots: true + build_args: 'c("--no-manual","--compact-vignettes=gs+qpdf")' diff --git a/.github/workflows/deploy-github-page.yml b/.github/workflows/deploy-github-page.yml index 18f06d8..37beac4 100644 --- a/.github/workflows/deploy-github-page.yml +++ b/.github/workflows/deploy-github-page.yml @@ -70,7 +70,6 @@ jobs: python -m pip install jupyterlite-xeus jupyter lite build \ --XeusAddon.prefix=${{ env.PREFIX }} \ - --XeusAddon.mounts=${{ env.PREFIX }}/share/jupyter/kernels/xr/resources:/share/jupyter/kernels/xr/resources \ --contents README.md \ --contents notebooks/xeus-r.ipynb \ --output-dir dist diff --git a/.gitignore b/.gitignore index 7d99c7f..134f237 100644 --- a/.gitignore +++ b/.gitignore @@ -50,3 +50,8 @@ bld Untitled*.ipynb *.log /*.ipynb +.Rproj.user +.Rhistory +hera.Rcheck +hera_*.tar.gz +hera_*.tgz diff --git a/CMakeLists.txt b/CMakeLists.txt index 1bfd8e9..f9068f5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -66,8 +66,15 @@ message(STATUS "R_LDFLAGS = ${R_LDFLAGS}") message(STATUS "R_LIBRARY_BASE = ${R_LIBRARY_BASE}") message(STATUS "R_LIBRARY_BLAS = ${R_LIBRARY_BLAS}") message(STATUS "R_LIBRARY_LAPACK = ${R_LIBRARY_LAPACK}") -message(STATUS "R_VERSION_MAJOR = ${R_VERSION_MAJOR}") -message(STATUS "R_VERSION_MINOR = ${R_VERSION_MINOR}") +message(STATUS "R_VERSION_MAJOR = ${R_VERSION_MAJOR}") +message(STATUS "R_VERSION_MINOR = ${R_VERSION_MINOR}") + +# Install R package hera +# ====================== + +message(STATUS "Installing R 📦 hera to ${R_HOME}/library") +execute_process(COMMAND ${R_COMMAND} CMD INSTALL --preclean --no-staged-install --build ../hera) +execute_process(COMMAND ${R_SCRIPT_COMMAND} -e "writeLines(paste('Installed 📦 hera version', as.character(packageVersion('hera'))))") # Configuration # ============= diff --git a/README.md b/README.md index b7aea4c..974be35 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,7 @@ [![Documentation Status](http://readthedocs.org/projects/xeus-r/badge/?version=latest)](https://xeus-r.readthedocs.io/en/latest/?badge=latest) [![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/jupyter-xeus/xeus-r/main?urlpath=/lab/tree/notebooks/xeus-r.ipynb) [![lite-badge](https://jupyterlite.rtfd.io/en/latest/_static/badge.svg)](https://jupyter-xeus.github.io/xeus-r/) +[![R-CMD-check](https://github.com/jupyter-xeus/xeus-r/actions/workflows/R-CMD-check.yaml/badge.svg)](https://github.com/jupyter-xeus/xeus-r/actions/workflows/R-CMD-check.yaml) `xeus-r` is a Jupyter kernel for the R programming language. @@ -38,7 +39,7 @@ Then you can compile the sources (replace `$CONDA_PREFIX` with a custom installa prefix if need be) ```bash -mkdir build && cd build +mkdir build && cd build && mkdir temp_r_lib cmake .. -D CMAKE_PREFIX_PATH=$CONDA_PREFIX -D CMAKE_INSTALL_PREFIX=$CONDA_PREFIX -D CMAKE_INSTALL_LIBDIR=lib make && make install ``` diff --git a/environment-dev.yml b/environment-dev.yml index 7bb10b1..5603c39 100644 --- a/environment-dev.yml +++ b/environment-dev.yml @@ -13,13 +13,14 @@ dependencies: - nlohmann_json=3.11.3 - r-base >=4 # Run dependencies - - r-rlang + - r-cli - r-evaluate - - r-jsonlite - r-glue - - r-cli - - r-repr - r-IRdisplay + - r-jsonlite + - r-r6 + - r-repr + - r-rlang # Test dependencies - pytest - jupyter_kernel_test>=0.5,<0.6 diff --git a/environment-wasm-host.yml b/environment-wasm-host.yml index 825b8e7..4d2000f 100644 --- a/environment-wasm-host.yml +++ b/environment-wasm-host.yml @@ -18,6 +18,8 @@ dependencies: # Run dependencies - r-rlang - r-evaluate + - r-glue + - r-r6 - r-jsonlite - r-glue - r-cli >=3.6.3 diff --git a/hera/.Rbuildignore b/hera/.Rbuildignore new file mode 100644 index 0000000..898a00a --- /dev/null +++ b/hera/.Rbuildignore @@ -0,0 +1,4 @@ +^LICENSE\.md$ +^.*\.Rproj$ +^\.Rproj\.user$ +^\.github$ diff --git a/hera/DESCRIPTION b/hera/DESCRIPTION new file mode 100644 index 0000000..98d1ede --- /dev/null +++ b/hera/DESCRIPTION @@ -0,0 +1,22 @@ +Package: hera +Title: Companion to the 'xeus-r' 'Jupyter' kernel for R +Version: 0.0.0.9000 +Authors@R: c( + person("Romain", "François", email = "romain@tada.science", role = c("aut", "cre")) + ) +Description: Set of R functions to be coupled with the xeus-r jupyter kernel for R. +License: MIT + file LICENSE +Encoding: UTF-8 +Roxygen: list(markdown = TRUE) +RoxygenNote: 7.3.2 +Imports: + cli, + evaluate, + glue, + IRdisplay, + jsonlite, + R6, + repr, + rlang, + tools, + utils diff --git a/hera/LICENSE b/hera/LICENSE new file mode 100644 index 0000000..46de013 --- /dev/null +++ b/hera/LICENSE @@ -0,0 +1,2 @@ +YEAR: 2025 +COPYRIGHT HOLDER: Quantstack diff --git a/hera/LICENSE.md b/hera/LICENSE.md new file mode 100644 index 0000000..75f3b16 --- /dev/null +++ b/hera/LICENSE.md @@ -0,0 +1,21 @@ +# MIT License + +Copyright (c) 2025 Quantstack + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/hera/NAMESPACE b/hera/NAMESPACE new file mode 100644 index 0000000..bb0c760 --- /dev/null +++ b/hera/NAMESPACE @@ -0,0 +1,18 @@ +# Generated by roxygen2: do not edit by hand + +export(View) +export(cell_options) +export(clear_output) +export(complete) +export(display_data) +export(is_xeusr) +importFrom(R6,R6Class) +importFrom(grDevices,pdf) +importFrom(grDevices,png) +importFrom(jsonlite,fromJSON) +importFrom(jsonlite,toJSON) +importFrom(jsonlite,unbox) +importFrom(rlang,caller_env) +importFrom(utils,capture.output) +importFrom(utils,head) +importFrom(utils,tail) diff --git a/hera/R/cell_options.R b/hera/R/cell_options.R new file mode 100644 index 0000000..f745a82 --- /dev/null +++ b/hera/R/cell_options.R @@ -0,0 +1,8 @@ +#' Options for current jupyter cell +#' +#' @inheritParams rlang::local_options +#' +#' @export +cell_options <- function(...) { + rlang::local_options(..., .frame = the$frame_cell_execute) +} diff --git a/share/jupyter/kernels/xr/resources/comm.R b/hera/R/comm.R similarity index 51% rename from share/jupyter/kernels/xr/resources/comm.R rename to hera/R/comm.R index 619bdc4..b85a558 100644 --- a/share/jupyter/kernels/xr/resources/comm.R +++ b/hera/R/comm.R @@ -1,9 +1,11 @@ +comm_target_env <- new.env() + .CommManager__register_target_callback <- function(comm, request) { target_callback <- comm_target_env[[request$content$target_name]] target_callback(comm, request) } -CommManagerClass <- R6::R6Class("CommManagerClass", +CommManagerClass <- R6Class("CommManagerClass", public = list( initialize = function() { private$targets <- new.env() @@ -12,90 +14,90 @@ CommManagerClass <- R6::R6Class("CommManagerClass", register_comm_target = function(target_name, callback) { private$targets[[target_name]] <- callback - invisible(.Call("CommManager__register_target", target_name, PACKAGE = "(embedding)")) - }, + invisible(hera_dot_call("CommManager__register_target", target_name, PACKAGE = "(embedding)")) + }, unregister_comm_target = function(target_name) { rm(list = target_name, private$targets) - invisible(.Call("CommManager__unregister_target", target_name, PACKAGE = "(embedding)")) - }, + invisible(hera_dot_call("CommManager__unregister_target", target_name)) + }, new_comm = function(target_name) { - xp <- .Call("CommManager__new_comm", target_name, PACKAGE = "(embedding)") + xp <- hera_dot_call("CommManager__new_comm", target_name) if (is.null(xp)) { stop(glue::glue("No target '{target_name}' registered")) } Comm$new(xp = xp) } - ), + ), private = list( - targets = NULL, + targets = NULL, comms = NULL ) ) CommManager <- CommManagerClass$new() -Comm <- R6::R6Class("Comm", +Comm <- R6Class("Comm", public = list( initialize = function(xp) { private$xp <- xp - }, + }, open = function(metadata = NULL, data = NULL) { - js_metadata <- jsonlite::toJSON(metadata) - js_data <- jsonlite::toJSON(data) + js_metadata <- toJSON(metadata) + js_data <- toJSON(data) - invisible(.Call("Comm__open", private$xp, js_metadata, js_data, PACKAGE = "(embedding)")) - }, + invisible(hera_dot_call("Comm__open", private$xp, js_metadata, js_data)) + }, close = function(metadata = NULL, data = NULL) { - js_metadata <- jsonlite::toJSON(metadata) - js_data <- jsonlite::toJSON(data) + js_metadata <- toJSON(metadata) + js_data <- toJSON(data) - invisible(.Call("Comm__close", private$xp, js_metadata, js_data, PACKAGE = "(embedding)")) - }, + invisible(hera_dot_call("Comm__close", private$xp, js_metadata, js_data)) + }, send = function(metadata = NULL, data = NULL) { - js_metadata <- jsonlite::toJSON(metadata) - js_data <- jsonlite::toJSON(data) + js_metadata <- toJSON(metadata) + js_data <- toJSON(data) - invisible(.Call("Comm__send", private$xp, js_metadata, js_data, PACKAGE = "(embedding)")) - }, + invisible(hera_dot_call("Comm__send", private$xp, js_metadata, js_data)) + }, on_close = function(handler) { private$close_handler <- handler - invisible(.Call("Comm__on_close", private$xp, handler, PACKAGE = "(embedding)")) - }, + invisible(hera_dot_call("Comm__on_close", private$xp, handler)) + }, on_message = function(handler) { private$message_handler <- handler - invisible(.Call("Comm__on_message", private$xp, handler, PACKAGE = "(embedding)")) + invisible(hera_dot_call("Comm__on_message", private$xp, handler)) } - ), + ), active = list( id = function() { - .Call("Comm__id", private$xp, PACKAGE = "(embedding)") - }, + hera_dot_call("Comm__id", private$xp) + }, target_name = function() { - .Call("Comm__target_name", private$xp, PACKAGE = "(embedding)") + hera_dot_call("Comm__target_name", private$xp) } ), - + private = list( - xp = NULL, - close_handler = NULL, + xp = NULL, + close_handler = NULL, message_handler = NULL ) ) -Message <- R6::R6Class("Message", +Message <- R6Class("Message", public = list( initialize = function(xp) { private$xp <- xp - }, + }, print = function() { print(cli::rule("$content")) @@ -110,23 +112,23 @@ Message <- R6::R6Class("Message", print(cli::rule("$metadata")) str(self$metadata) } - ), + ), active = list( content = function() { - jsonlite::fromJSON(.Call("Message__get_content", private$xp, PACKAGE = "(embedding)")) - }, + fromJSON(hera_dot_call("Message__get_content", private$xp)) + }, header = function() { - jsonlite::fromJSON(.Call("Message__get_header", private$xp, PACKAGE = "(embedding)")) - }, + fromJSON(hera_dot_call("Message__get_header", private$xp)) + }, parent_header = function() { - jsonlite::fromJSON(.Call("Message__get_parent_header", private$xp, PACKAGE = "(embedding)")) - }, + fromJSON(hera_dot_call("Message__get_parent_header", private$xp)) + }, metadata = function() { - jsonlite::fromJSON(.Call("Message__get_metadata", private$xp, PACKAGE = "(embedding)")) + fromJSON(hera_dot_call("Message__get_metadata", private$xp)) } ), diff --git a/hera/R/completion.R b/hera/R/completion.R new file mode 100644 index 0000000..a318ec2 --- /dev/null +++ b/hera/R/completion.R @@ -0,0 +1,51 @@ +triple_colon <- function(pkg, fun) { + eval(rlang::call2(":::", as.symbol(pkg), as.symbol(fun))) +} + +utils___assignLineBuffer <- triple_colon("utils", ".assignLinebuffer") +utils___assignEnd <- triple_colon("utils", ".assignEnd") +utils___guessTokenFromLine <- triple_colon("utils", ".guessTokenFromLine") +utils___completeToken <- triple_colon("utils", ".completeToken") +utils___retrieveCompletions <- triple_colon("utils", ".retrieveCompletions") + +#' Code completion +#' +#' @param code R code to complete +#' @param cursor_pos position of the cursor +#' +#' @return a list that contains potential completions as the first item +#' +#' @export +complete <- function(code, cursor_pos = nchar(code)) { + # Find which line we're on and position within that line + lines <- strsplit(code, '\n', fixed = TRUE)[[1]] + chars_before_line <- 0L + for (line in lines) { + new_cursor_pos <- cursor_pos - nchar(line) - 1L # -1 for the newline + if (new_cursor_pos < 0L) { + break + } + cursor_pos <- new_cursor_pos + chars_before_line <- chars_before_line + nchar(line) + 1L + } + + # guard from errors when completion is invoked in empty cells + if (is.null(line)) { + line <- '' + } + + utils___assignLineBuffer(line) + utils___assignEnd(cursor_pos) + + info <- utils___guessTokenFromLine(update = FALSE) + utils___guessTokenFromLine() + utils___completeToken() + + start_position <- chars_before_line + info$start + comps <- utils___retrieveCompletions() + + list( + comps, + c(start_position, start_position + nchar(info$token)) + ) +} diff --git a/share/jupyter/kernels/xr/resources/execute.R b/hera/R/execute.R similarity index 72% rename from share/jupyter/kernels/xr/resources/execute.R rename to hera/R/execute.R index b744cad..1861d36 100644 --- a/share/jupyter/kernels/xr/resources/execute.R +++ b/hera/R/execute.R @@ -1,7 +1,3 @@ -last_plot <- NULL -last_visible <- TRUE -last_error <- NULL - handle_message <- function(msg) { publish_stream("stderr", conditionMessage(msg)) } @@ -47,16 +43,16 @@ trim_rlang_error <- function(e) { handle_error <- function(e) { if (inherits(e, "rlang_error") && isNamespaceLoaded("rlang")) { e <- trim_rlang_error(e) - assign("last_error", e, rlang:::the) + assign("last_error", e, triple_colon("rlang", "the")) trace_back <- c( cli::col_red("--- Error"), - format(e, backtrace = FALSE), + format(e, backtrace = FALSE), "", - cli::col_red("--- Traceback"), + cli::col_red("--- Traceback"), format(e$trace) ) - last_error <<- structure(list(ename = "ERROR", evalue = "", trace_back), class = "error_reply") + the$last_error <- structure(list(ename = "ERROR", evalue = "", trace_back), class = "error_reply") } else { sys_calls <- sys.calls() sys_calls <- head(tail(sys_calls, -16), -3) @@ -65,12 +61,12 @@ handle_error <- function(e) { evalue <- paste(conditionMessage(e), collapse = "\n") trace_back <- c( cli::col_red("--- Error"), - evalue, + evalue, "", - cli::col_red("--- Traceback (most recent call last)"), + cli::col_red("--- Traceback (most recent call last)"), stack ) - last_error <<- structure(list(ename = "ERROR", evalue = evalue, trace_back), class = "error_reply") + the$last_error <- structure(list(ename = "ERROR", evalue = evalue, trace_back), class = "error_reply") } } @@ -79,7 +75,7 @@ handle_value <- function(obj, visible) { if (visible && inherits(obj, "ggplot")) { print(obj) - last_visible <<- FALSE + the$last_visible <- FALSE } } @@ -89,12 +85,12 @@ handle_graphics <- function(plot) { attr(plot, ".irkernel_height") <- getOption('repr.plot.height', repr::repr_option_defaults$repr.plot.height) attr(plot, ".irkernel_res") <- getOption('repr.plot.res', repr::repr_option_defaults$repr.plot.res) attr(plot, ".irkernel_ppi") <- attr(plot, ".irkernel_res") / getOption('jupyter.plot_scale', 2) - - if (!plot_builds_upon(last_plot, plot)) { - send_plot(last_plot) + + if (!plot_builds_upon(the$last_plot, plot)) { + send_plot(the$last_plot) } - last_plot <<- plot + the$last_plot <- plot } send_plot <- function(plot) { @@ -126,52 +122,54 @@ send_plot <- function(plot) { display_data(data, metadata) } +# currently not exported, because it is only meant to be called +# from xeus-r / interpreter::execute_request_impl execute <- function(code, execution_counter, silent = FALSE) { - last_error <<- NULL - + the$last_error <- NULL + parsed <- tryCatch( - parse(text = code), + parse(text = code), error = function(e) { msg <- paste(conditionMessage(e), collapse = "\n") - last_error <<- structure(list(ename = "PARSE ERROR", evalue = msg), class = "error_reply") + the$last_error <- structure(list(ename = "PARSE ERROR", evalue = msg), class = "error_reply") } ) - if (!is.null(last_error)) return(last_error) - + if (!is.null(the$last_error)) return(the$last_error) + output_handler <- if (silent) { evaluate::new_output_handler() } else { evaluate::new_output_handler( - text = function(txt) publish_stream("stdout", txt), + text = function(txt) publish_stream("stdout", txt), graphics = handle_graphics, - message = handle_message, - warning = handle_warning, - error = handle_error, + message = handle_message, + warning = handle_warning, + error = handle_error, value = handle_value - ) + ) } - - last_plot <<- NULL - last_visible <<- FALSE + + the$last_plot <- NULL + the$last_visible <- FALSE filename <- glue::glue("[{execution_counter}]") - frame_cell_execute <<- environment() + the$frame_cell_execute <- environment() evaluate::evaluate( code, envir = globalenv(), output_handler = output_handler, - stop_on_error = 1L, + stop_on_error = 1L, filename = filename ) - if (!is.null(last_error)) return(last_error) + if (!is.null(the$last_error)) return(the$last_error) - if (!silent && !is.null(last_plot)) { - tryCatch(send_plot(last_plot), error = handle_error) + if (!silent && !is.null(the$last_plot)) { + tryCatch(send_plot(the$last_plot), error = handle_error) } - if (!is.null(last_error)) return(last_error) + if (!is.null(the$last_error)) return(the$last_error) - if (isTRUE(last_visible)) { + if (isTRUE(the$last_visible)) { obj <- .Last.value # TODO: This probably needs to be generalized @@ -180,10 +178,10 @@ execute <- function(code, execution_counter, silent = FALSE) { } else { "text/plain" } - + bundle <- IRdisplay::prepare_mimebundle(obj, mimetypes = mimetypes) - - structure(class = "execution_result", + + structure(class = "execution_result", list(toJSON(bundle$data), toJSON(bundle$metadata)) ) } diff --git a/share/jupyter/kernels/xr/resources/inspect.R b/hera/R/inspect.R similarity index 97% rename from share/jupyter/kernels/xr/resources/inspect.R rename to hera/R/inspect.R index ac790c2..f3ba2bc 100644 --- a/share/jupyter/kernels/xr/resources/inspect.R +++ b/hera/R/inspect.R @@ -10,7 +10,7 @@ inspect <- function(code, cursor_pos) { # check them by a loop. Use get since R CMD check does not like ::: token <- '' for (i in seq(cursor_pos, nchar(code))) { - token_candidate <- utils:::.guessTokenFromLine(code, i) + token_candidate <- utils___guessTokenFromLine(code, i) if (nchar(token_candidate) == 0) break token <- token_candidate } diff --git a/share/jupyter/kernels/xr/resources/log.R b/hera/R/log.R similarity index 81% rename from share/jupyter/kernels/xr/resources/log.R rename to hera/R/log.R index 25e0644..1ccecce 100644 --- a/share/jupyter/kernels/xr/resources/log.R +++ b/hera/R/log.R @@ -3,7 +3,7 @@ logger <- function(level, name) { function(...) { if (isTRUE(getOption('jupyter.log_level') >= level)) { msg <- glue::glue(...) - .Call("xeusr_log", name, msg, PACKAGE = "(embedding)") + hera_dot_call("xeusr_log", name, msg) } invisible(NULL) } diff --git a/hera/R/routines.R b/hera/R/routines.R new file mode 100644 index 0000000..8b6b695 --- /dev/null +++ b/hera/R/routines.R @@ -0,0 +1,48 @@ +publish_stream <- function(name, text) { + hera_dot_call("xeusr_publish_stream", name, text) +} + +#' Display data +#' +#' @param data data to display +#' @param metadata potential metadata +#' +#' @export +display_data <- function(data = NULL, metadata = NULL) { + invisible(hera_dot_call("xeusr_display_data", toJSON(data), toJSON(metadata))) +} + +update_display_data <- function(data = NULL, metadata = NULL) { + invisible(hera_dot_call("xeusr_update_display_data", toJSON(data), toJSON(metadata))) +} + +kernel_info_request <- function() { + hera_dot_call("xeusr_kernel_info_request") +} + + +#' Clear output +#' +#' @param wait Should this wait +#' +#' @return NULL invisibly +#' @export +clear_output <- function(wait = FALSE) { + invisible(hera_dot_call("xeusr_clear_output", isTRUE(wait))) +} + +is_complete_request <- function(code) { + hera_dot_call("xeusr_is_complete_request", code) +} + +#' View +#' +#' @param x something to display +#' @param title title of the display +#' +#' @export +View <- function(x, title) { + if (!missing(title)) IRdisplay::display_text(title) + IRdisplay::display(x) + invisible(x) +} diff --git a/share/jupyter/kernels/xr/resources/utils.R b/hera/R/utils.R similarity index 79% rename from share/jupyter/kernels/xr/resources/utils.R rename to hera/R/utils.R index 278e49c..3556850 100644 --- a/share/jupyter/kernels/xr/resources/utils.R +++ b/hera/R/utils.R @@ -13,11 +13,11 @@ namedlist <- function() { } set_last_value <- function(obj, visible) { - last_visible <<- visible + the$last_visible <- visible - unlockBinding(".Last.value", .BaseNamespaceEnv) + get("unlockBinding", envir = baseenv())(".Last.value", .BaseNamespaceEnv) assign(".Last.value", obj, .BaseNamespaceEnv) - lockBinding(".Last.value", .BaseNamespaceEnv) + get("lockBinding", envir = baseenv())(".Last.value", .BaseNamespaceEnv) } # borrowed from IRkernel @@ -25,9 +25,9 @@ plot_builds_upon <- function(prev, current) { if (is.null(prev)) { return(TRUE) } - + lprev <- length(prev[[1]]) lcurrent <- length(current[[1]]) - + lcurrent >= lprev && identical(current[[1]][1:lprev], prev[[1]][1:lprev]) } diff --git a/hera/R/zzz.R b/hera/R/zzz.R new file mode 100644 index 0000000..b940939 --- /dev/null +++ b/hera/R/zzz.R @@ -0,0 +1,116 @@ +#' @importFrom grDevices pdf png +#' @importFrom jsonlite toJSON unbox fromJSON +#' @importFrom utils head tail capture.output +#' @importFrom R6 R6Class +#' @importFrom rlang caller_env +NULL + +print_vignette <- function(x, ...) { + file <- x$PDF + if (nzchar(file) == 0) { + warning(gettextf("vignette %s has no PDF/HTML", sQuote(x$Topic)), call. = FALSE, domain = NA) + return(invisible(x)) + } + + ext <- tolower(tools::file_ext(file)) + if (ext == "pdf") { + warning("can't display pdf vignette yet") + return(invisible(x)) + } + + if (ext == "html") { + html <- readLines(file.path(x$Dir, "doc", file)) + + display_data( + data = list( + "text/html" = paste(html, collapse = "\n") + ), + metadata = list( + "text/html" = list(isolated = TRUE) + ) + ) + } + + invisible(x) +} + +the <- NULL + +.onLoad <- function(libname, pkgname) { + # - verify this is running within xeus-r + # - handshake + the <<- new.env() + the$frame_cell_execute <- NULL + the$last_plot <- NULL + the$last_visible <- TRUE + the$last_error <- NULL + + ns_utils <- asNamespace("utils") + get("unlockBinding", envir = baseenv())("print.vignette", ns_utils) + + assign("print.vignette", print_vignette, ns_utils) + get("lockBinding", envir = baseenv())("print.vignette", ns_utils) + + init_options() +} + +init_options <- function() { + options( + device = get_null_device(), + cli.num_colors = 256L, + jupyter.plot_mimetypes = c('text/plain', 'image/png'), + jupyter.plot_scale = 2, + + jupyter.rich_display = TRUE, + jupyter.base_display_func = display_data, + jupyter.clear_output_func = clear_output + ) + + repos <- getOption('repos') + if (identical(repos, c(CRAN = '@CRAN@'))) { + repos[['CRAN']] <- 'https://cran.r-project.org' + options(repos = repos) + } +} + +NAMESPACE <- environment() +hera_call <- function(fn, ...) { + get(fn, envir = NAMESPACE)(...) +} + +hera_new <- function(class, xp) { + get(class, envir = NAMESPACE)$new(xp) +} + +#' Is this a running xeusr jupyter kernel +#' +#' @return TRUE if the current session is running in a xeusr kernel +#' @export +is_xeusr <- function() { + embedding <- getLoadedDLLs()[["(embedding)"]] + !is.null(embedding) && "xeusr_kernel_info_request" %in% names(getDLLRegisteredRoutines(embedding)$.Call) +} + +hera_dot_call <- function(fn, ..., error_call = caller_env()) { + call <- rlang::call2(".Call", fn, ..., PACKAGE = "(embedding)") + + if (!is_xeusr()) { + cli::cli_abort(c( + "The {.val {fn}} routine must be called inside a xeusr kernel.", + i = "Full internal call to the xeusr routine:", + " " = "{deparse(call)}" + ), call = error_call) + } + eval.parent(call) +} + +get_null_device <- function() { + os <- get_os() + + ok_device <- switch(os, win = png, osx = pdf, unix = png, wasm = png) + null_filename <- switch(os, win = 'NUL', osx = NULL, unix = '/dev/null', wasm = '/tmp/null') + + null_device <- function(filename = null_filename, ...) ok_device(filename, ...) + null_device +} + diff --git a/hera/hera.Rproj b/hera/hera.Rproj new file mode 100644 index 0000000..270314b --- /dev/null +++ b/hera/hera.Rproj @@ -0,0 +1,21 @@ +Version: 1.0 + +RestoreWorkspace: Default +SaveWorkspace: Default +AlwaysSaveHistory: Default + +EnableCodeIndexing: Yes +UseSpacesForTab: Yes +NumSpacesForTab: 2 +Encoding: UTF-8 + +RnwWeave: Sweave +LaTeX: pdfLaTeX + +AutoAppendNewline: Yes +StripTrailingWhitespace: Yes + +BuildType: Package +PackageUseDevtools: Yes +PackageInstallArgs: --no-multiarch --with-keep.source +PackageRoxygenize: rd,collate,namespace diff --git a/hera/man/View.Rd b/hera/man/View.Rd new file mode 100644 index 0000000..7875136 --- /dev/null +++ b/hera/man/View.Rd @@ -0,0 +1,16 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/routines.R +\name{View} +\alias{View} +\title{View} +\usage{ +View(x, title) +} +\arguments{ +\item{x}{something to display} + +\item{title}{title of the display} +} +\description{ +View +} diff --git a/hera/man/cell_options.Rd b/hera/man/cell_options.Rd new file mode 100644 index 0000000..4dda0cc --- /dev/null +++ b/hera/man/cell_options.Rd @@ -0,0 +1,16 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/cell_options.R +\name{cell_options} +\alias{cell_options} +\title{Options for current jupyter cell} +\usage{ +cell_options(...) +} +\arguments{ +\item{...}{For \code{local_options()} and \code{push_options()}, named +values defining new option values. For \code{peek_options()}, strings +or character vectors of option names.} +} +\description{ +Options for current jupyter cell +} diff --git a/hera/man/clear_output.Rd b/hera/man/clear_output.Rd new file mode 100644 index 0000000..5df0b34 --- /dev/null +++ b/hera/man/clear_output.Rd @@ -0,0 +1,17 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/routines.R +\name{clear_output} +\alias{clear_output} +\title{Clear output} +\usage{ +clear_output(wait = FALSE) +} +\arguments{ +\item{wait}{Should this wait} +} +\value{ +NULL invisibly +} +\description{ +Clear output +} diff --git a/hera/man/complete.Rd b/hera/man/complete.Rd new file mode 100644 index 0000000..2054f06 --- /dev/null +++ b/hera/man/complete.Rd @@ -0,0 +1,19 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/completion.R +\name{complete} +\alias{complete} +\title{Code completion} +\usage{ +complete(code, cursor_pos = nchar(code)) +} +\arguments{ +\item{code}{R code to complete} + +\item{cursor_pos}{position of the cursor} +} +\value{ +a list that contains potential completions as the first item +} +\description{ +Code completion +} diff --git a/hera/man/display_data.Rd b/hera/man/display_data.Rd new file mode 100644 index 0000000..e37b9a7 --- /dev/null +++ b/hera/man/display_data.Rd @@ -0,0 +1,16 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/routines.R +\name{display_data} +\alias{display_data} +\title{Display data} +\usage{ +display_data(data = NULL, metadata = NULL) +} +\arguments{ +\item{data}{data to display} + +\item{metadata}{potential metadata} +} +\description{ +Display data +} diff --git a/hera/man/is_xeusr.Rd b/hera/man/is_xeusr.Rd new file mode 100644 index 0000000..64e7cd1 --- /dev/null +++ b/hera/man/is_xeusr.Rd @@ -0,0 +1,14 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/zzz.R +\name{is_xeusr} +\alias{is_xeusr} +\title{Is this a running xeusr jupyter kernel} +\usage{ +is_xeusr() +} +\value{ +TRUE if the current session is running in a xeusr kernel +} +\description{ +Is this a running xeusr jupyter kernel +} diff --git a/share/jupyter/kernels/xr/resources/call.R b/share/jupyter/kernels/xr/resources/call.R deleted file mode 100644 index 5600131..0000000 --- a/share/jupyter/kernels/xr/resources/call.R +++ /dev/null @@ -1,7 +0,0 @@ -.xeus_call <- function(fn, ...) { - get(fn, envir = .xeus_env)(...) -} - -.xeus_new <- function(class, xp) { - get(class, envir = .xeus_env)$new(xp) -} diff --git a/share/jupyter/kernels/xr/resources/completion.R b/share/jupyter/kernels/xr/resources/completion.R deleted file mode 100644 index 9a5c7fa..0000000 --- a/share/jupyter/kernels/xr/resources/completion.R +++ /dev/null @@ -1,35 +0,0 @@ -# This is mostly inspired from IRkernel::completions() -complete <- function(code, cursor_pos = nchar(code)) { - # ---- TODO: this should be done on the C++ side - - # Find which line we're on and position within that line - lines <- strsplit(code, '\n', fixed = TRUE)[[1]] - chars_before_line <- 0L - for (line in lines) { - new_cursor_pos <- cursor_pos - nchar(line) - 1L # -1 for the newline - if (new_cursor_pos < 0L) { - break - } - cursor_pos <- new_cursor_pos - chars_before_line <- chars_before_line + nchar(line) + 1L - } - - # guard from errors when completion is invoked in empty cells - if (is.null(line)) { - line <- '' - } - - utils:::.assignLinebuffer(line) - utils:::.assignEnd(cursor_pos) - - info <- utils:::.guessTokenFromLine(update = FALSE) - utils:::.guessTokenFromLine() - utils:::.completeToken() - - start_position <- chars_before_line + info$start - comps <- utils:::.retrieveCompletions() - - # TODO: use jsonlite::toJSON() here - list(comps, c(start_position, start_position + nchar(info$token))) - -} \ No newline at end of file diff --git a/share/jupyter/kernels/xr/resources/configure.R b/share/jupyter/kernels/xr/resources/configure.R deleted file mode 100644 index 39819d2..0000000 --- a/share/jupyter/kernels/xr/resources/configure.R +++ /dev/null @@ -1,42 +0,0 @@ -get_null_device <- function() { - os <- get_os() - - ok_device <- switch(os, win = png, osx = pdf, unix = png, wasm = png) - null_filename <- switch(os, win = 'NUL', osx = NULL, unix = '/dev/null', wasm = '/tmp/null') - - null_device <- function(filename = null_filename, ...) ok_device(filename, ...) - null_device -} - -init_options <- function() { - options( - device = get_null_device(), - cli.num_colors = 256L, - jupyter.plot_mimetypes = c('text/plain', 'image/png'), - jupyter.plot_scale = 2, - - jupyter.rich_display = TRUE, - jupyter.base_display_func = display_data, - jupyter.clear_output_func = clear_output - ) - - repos <- getOption('repos') - if (identical(repos, c(CRAN = '@CRAN@'))) { - repos[['CRAN']] <- 'https://cran.r-project.org' - options(repos = repos) - } -} - -configure <- function() { - pos <- which(search() == "tools:xeusr") - - attachNamespace("IRdisplay", pos = pos + 1) - attachNamespace("glue", pos = pos + 1) - attachNamespace("jsonlite", pos = pos + 1) - - # setMethod(jsonlite:::asJSON, "shiny.tag", function(x, ...) { - # jsonlite:::asJSON(as.character(x), ...) - # }) - - init_options() -} diff --git a/share/jupyter/kernels/xr/resources/jsonlite.R b/share/jupyter/kernels/xr/resources/jsonlite.R deleted file mode 100644 index e69de29..0000000 diff --git a/share/jupyter/kernels/xr/resources/options.R b/share/jupyter/kernels/xr/resources/options.R deleted file mode 100644 index 05ec327..0000000 --- a/share/jupyter/kernels/xr/resources/options.R +++ /dev/null @@ -1,5 +0,0 @@ -frame_cell_execute <- NULL - -cell_options <- function(...) { - rlang::local_options(..., .frame = frame_cell_execute) -} diff --git a/share/jupyter/kernels/xr/resources/routines.R b/share/jupyter/kernels/xr/resources/routines.R deleted file mode 100644 index 9f11fc4..0000000 --- a/share/jupyter/kernels/xr/resources/routines.R +++ /dev/null @@ -1,66 +0,0 @@ -publish_stream <- function(name, text) { - .Call("xeusr_publish_stream", name, text, PACKAGE = "(embedding)") -} - -display_data <- function(data = NULL, metadata = NULL) { - invisible(.Call("xeusr_display_data", jsonlite::toJSON(data), jsonlite::toJSON(metadata), PACKAGE = "(embedding)")) -} - -update_display_data <- function(data = NULL, metadata = NULL) { - invisible(.Call("xeusr_update_display_data", jsonlite::toJSON(data), jsonlite::toJSON(metadata), PACKAGE = "(embedding)")) -} - -kernel_info_request <- function() { - .Call("xeusr_kernel_info_request", PACKAGE = "(embedding)") -} - -clear_output <- function(wait = FALSE) { - invisible(.Call("xeusr_clear_output", isTRUE(wait), PACKAGE = "(embedding)")) -} - -is_complete_request <- function(code) { - .Call("xeusr_is_complete_request", code, PACKAGE = "(embedding)") -} - -cell_options <- function(...) { - rlang::local_options(..., .frame = .xeusr_private_env$frame_cell_execute) -} - -View <- function(x, title) { - if (!missing(title)) IRdisplay::display_text(title) - IRdisplay::display(x) - invisible(x) -} - -ns_utils <- asNamespace("utils") -unlockBinding("print.vignette", ns_utils) -print.vignette <- function(x, ...) { - file <- x$PDF - if (nzchar(file) == 0) { - warning(gettextf("vignette %s has no PDF/HTML", sQuote(x$Topic)), call. = FALSE, domain = NA) - return(invisible(x)) - } - - ext <- tolower(tools::file_ext(file)) - if (ext == "pdf") { - warning("can't display pdf vignette yet") - return(invisible(x)) - } - - if (ext == "html") { - html <- readLines(file.path(x$Dir, "doc", file)) - - display_data( - data = list( - "text/html" = paste(html, collapse = "\n") - ), - metadata = list( - "text/html" = list(isolated = TRUE) - ) - ) - } - - invisible(x) -} -assign("print.vignette", print.vignette, ns_utils) -lockBinding("print.vignette", ns_utils) diff --git a/share/jupyter/kernels/xr/resources/setup.R b/share/jupyter/kernels/xr/resources/setup.R deleted file mode 100644 index ddbd1b2..0000000 --- a/share/jupyter/kernels/xr/resources/setup.R +++ /dev/null @@ -1,25 +0,0 @@ -local({ - - attach(new.env(), "tools:xeusr", pos = 2L) - .xeus_env <- as.environment("tools:xeusr") - assign(".xeus_env", .xeus_env, pos = .xeus_env) - - # Sys.which is not available in WebAssembly - if (grepl("emscripten", R.version$os)) { - here <- file.path( - "share", "jupyter", "kernels", "xr", "resources" - ) - } else { - here <- file.path( - dirname(Sys.which("xr")), - "..", "share", "jupyter", "kernels", "xr", "resources" - ) - } - - files <- setdiff(list.files(here), "setup.R") - - for (f in files) { - sys.source(file.path(here, f), envir = .xeus_env) - } - -}) diff --git a/src/routines.cpp b/src/routines.cpp index 212c2bf..e55aaae 100644 --- a/src/routines.cpp +++ b/src/routines.cpp @@ -99,7 +99,7 @@ SEXP CommManager__register_target(SEXP name_) { SEXP xptr_comm = PROTECT(R_MakeExternalPtr( reinterpret_cast(ptr_comm), R_NilValue, R_NilValue )); - SEXP r6_comm = PROTECT(r::new_r6("Comm", xptr_comm)); + SEXP r6_comm = PROTECT(r::new_hera_r6("Comm", xptr_comm)); // request auto ptr_request = new xeus::xmessage(std::move(request)); @@ -110,10 +110,10 @@ SEXP CommManager__register_target(SEXP name_) { delete reinterpret_cast(R_ExternalPtrAddr(xp)); }, FALSE); - SEXP r6_request = PROTECT(r::new_r6("Message", xptr_request)); + SEXP r6_request = PROTECT(r::new_hera_r6("Message", xptr_request)); // callback - r::invoke_xeusr_fn(".CommManager__register_target_callback", r6_comm, r6_request); + r::invoke_hera_fn(".CommManager__register_target_callback", r6_comm, r6_request); UNPROTECT(4); }; @@ -204,13 +204,12 @@ class Comm_Message_handler { SEXP call = PROTECT(r::r_call( m_handler, - r::new_r6("Message", xptr_message)) + r::new_hera_r6("Message", xptr_message)) ); Rf_eval(call, R_GlobalEnv); UNPROTECT(2); - } private: diff --git a/src/rtools.hpp b/src/rtools.hpp index a423134..f6d5450 100644 --- a/src/rtools.hpp +++ b/src/rtools.hpp @@ -34,27 +34,31 @@ SEXP r_call(SEXP head, Types... tail) { } template -SEXP invoke_xeusr_fn(const char* f, Types... args) { - SEXP sym_xeus_call = Rf_install(".xeus_call"); - - SEXP call = PROTECT(r_call(sym_xeus_call, Rf_mkString(f), args...)); +SEXP invoke_hera_fn(const char* f, Types... args) { + SEXP sym_hera = Rf_install("hera"); + SEXP sym_hera_call = Rf_install("hera_call"); + SEXP sym_triple_colon = Rf_install(":::"); + + SEXP call_triple_colon = PROTECT(r_call(sym_triple_colon, sym_hera, sym_hera_call)); + SEXP call = PROTECT(r_call(call_triple_colon, Rf_mkString(f), args...)); SEXP result = Rf_eval(call, R_GlobalEnv); - UNPROTECT(1); + UNPROTECT(2); return result; } -inline SEXP new_r6(const char* klass, SEXP xp) { - SEXP sym_new_r6 = Rf_install(".xeus_new"); +inline SEXP new_hera_r6(const char* klass, SEXP xp) { + SEXP sym_hera = Rf_install("hera"); + SEXP sym_hera_new = Rf_install("hera_new"); + SEXP sym_triple_colon = Rf_install(":::"); - SEXP call = PROTECT(r_call(sym_new_r6, Rf_mkString(klass), xp)); + SEXP call = PROTECT(r_call(sym_triple_colon, Rf_mkString(klass), xp)); SEXP result = Rf_eval(call, R_GlobalEnv); - UNPROTECT(1); + UNPROTECT(2); return result; } - } } diff --git a/src/xinterpreter.cpp b/src/xinterpreter.cpp index 2a5a62a..ca53cc2 100644 --- a/src/xinterpreter.cpp +++ b/src/xinterpreter.cpp @@ -105,7 +105,7 @@ void interpreter::execute_request_impl( SEXP execution_counter_ = PROTECT(Rf_ScalarInteger(execution_count)); SEXP silent_ = PROTECT(Rf_ScalarLogical(config.silent)); - SEXP result = r::invoke_xeusr_fn("execute", code_, execution_counter_, silent_); + SEXP result = r::invoke_hera_fn("execute", code_, execution_counter_, silent_); if (Rf_inherits(result, "error_reply")) { std::string evalue = CHAR(STRING_ELT(VECTOR_ELT(result, 0), 0)); @@ -140,35 +140,17 @@ void interpreter::execute_request_impl( void interpreter::configure_impl() { - std::stringstream ss; - -#ifndef __EMSCRIPTEN__ - // Sys.which is not available in WebAssembly - SEXP sym_Sys_which = Rf_install("Sys.which"); - SEXP sym_dirname = Rf_install("dirname"); - SEXP str_xr = Rf_mkString("xr"); - SEXP call_Sys_which = PROTECT(Rf_lang2(sym_Sys_which, str_xr)); - SEXP call = PROTECT(Rf_lang2(sym_dirname, call_Sys_which)); - SEXP dir_xr = Rf_eval(call, R_GlobalEnv); - ss << CHAR(STRING_ELT(dir_xr, 0)) << "/../share/jupyter/kernels/xr/resources/setup.R"; -#else - ss << "/share/jupyter/kernels/xr/resources/setup.R"; -#endif - - SEXP setup_R_code_path = PROTECT(Rf_mkString(ss.str().c_str())); - - SEXP sym_source = Rf_install("source"); - SEXP call_source = PROTECT(Rf_lang2(sym_source, setup_R_code_path)); - - Rf_eval(call_source, R_GlobalEnv); - - r::invoke_xeusr_fn("configure"); + SEXP sym_library = Rf_install("require"); + SEXP str_hera = PROTECT(Rf_mkString("hera")); + SEXP sym_quietly = Rf_install("quietly"); + SEXP call_library_hera = PROTECT(xeus_r::r::r_call(sym_library, str_hera, /* quietly = */ Rf_ScalarLogical(TRUE))); + SET_TAG(CDDR(call_library_hera), sym_quietly); + SEXP out = PROTECT(Rf_eval(call_library_hera, R_GlobalEnv)); + if (LOGICAL_ELT(out, 0) == FALSE) { + // TODO: suicide the kernel because hera is not installed + } -#ifndef __EMSCRIPTEN__ - UNPROTECT(4); -#else - UNPROTECT(2); -#endif + UNPROTECT(3); } nl::json interpreter::is_complete_request_impl(const std::string& code_) @@ -238,7 +220,7 @@ nl::json interpreter::complete_request_impl(const std::string& code, int cursor_ SEXP code_ = PROTECT(Rf_mkString(code.c_str())); SEXP cursor_pos_ = PROTECT(Rf_ScalarInteger(cursor_pos)); - SEXP result = PROTECT(r::invoke_xeusr_fn("complete", code_, cursor_pos_)); + SEXP result = PROTECT(r::invoke_hera_fn("complete", code_, cursor_pos_)); auto matches = json_from_character_vector(VECTOR_ELT(result, 0)); int cursor_start = INTEGER_ELT(VECTOR_ELT(result, 1), 0); @@ -254,11 +236,10 @@ nl::json interpreter::complete_request_impl(const std::string& code, int cursor_ nl::json interpreter::inspect_request_impl(const std::string& code, int cursor_pos, int /*detail_level*/) { - SEXP code_ = PROTECT(Rf_mkString(code.c_str())); SEXP cursor_pos_ = PROTECT(Rf_ScalarInteger(cursor_pos)); - SEXP result = PROTECT(r::invoke_xeusr_fn("inspect", code_, cursor_pos_)); + SEXP result = PROTECT(r::invoke_hera_fn("inspect", code_, cursor_pos_)); bool found = LOGICAL_ELT(VECTOR_ELT(result, 0), 0); if (!found) { UNPROTECT(3); diff --git a/test/test_xr_kernel.py b/test/test_xr_kernel.py index fc0a191..5a8ea9d 100644 --- a/test/test_xr_kernel.py +++ b/test/test_xr_kernel.py @@ -21,7 +21,10 @@ class KernelTests(jupyter_kernel_test.KernelTests): completion_samples = [ {"text": "rnorm(", "matches": {"n=", "mean=", "sd="}} ] - code_execute_result = [{"code": "6*7", "result": ["[1] 42"]}] + code_execute_result = [ + {"code": "6*7" , "result": ["[1] 42"]}, + {"code": "is_xeusr()", "result": ["[1] TRUE"]} + ] code_display_data = [ {"code": "plot(0)", "mime": "image/png"}, {"code": "ggplot2::ggplot(iris, ggplot2::aes(Sepal.Length, Sepal.Width)) + ggplot2::geom_point()", "mime": "image/png"},