diff --git a/R/ace-autocomplete.R b/R/ace-autocomplete.R index 1f0be0a..028b128 100644 --- a/R/ace-autocomplete.R +++ b/R/ace-autocomplete.R @@ -1,8 +1,8 @@ #' Enable Code Completion for an Ace Code Input -#' +#' #' This function dynamically auto complete R code pieces using built-in function #' \code{utils:::.win32consoleCompletion}. Please see \code{\link[utils]{rcompgen}} for details. -#' +#' #' @details #' You can implement your own code completer by listening to \code{input$shinyAce__hint} #' where is the \code{aceEditor} id. The input contains @@ -10,24 +10,30 @@ #' \item \code{linebuffer}: Code/Text at current editing line #' \item \code{cursorPosition}: Current cursor position at this line #' } -#' +#' #' @param inputId The id of the input object #' @param session The \code{session} object passed to function given to shinyServer #' @return An observer reference class object that is responsible for offering code completion. #' See \code{\link[shiny]{observe}} for more details. You can use \code{suspend} or \code{destroy} #' to pause to stop dynamic code completion. -#' @export +#' @export aceAutocomplete <- function(inputId, session = shiny::getDefaultReactiveDomain()) { shiny::observe({ - value <- session$input[[paste0(inputId, "_hint")]] - if(is.null(value)) return(NULL) - + value <- session$input[[paste0(inputId, "_shinyAce_hint")]] + if (is.null(value)) return(NULL) + utilEnv <- environment(utils::alarm) w32 <- get(".win32consoleCompletion", utilEnv) - - comps <- list(id = session$ns(inputId), - codeCompletions = w32(value$linebuffer, value$cursorPosition)$comps) - + codeCompletions <- w32(value$linebuffer, value$cursorPosition$col)$comps + codeCompletions <- strsplit(codeCompletions, " ", fixed = TRUE)[[1]] + codeCompletions <- lapply(codeCompletions, function(completion) { + list(name = completion, value = completion, meta = "R") + }) + + comps <- list( + id = inputId, + codeCompletions = jsonlite::toJSON(codeCompletions, auto_unbox = TRUE) + ) session$sendCustomMessage('shinyAce', comps) }) -} \ No newline at end of file +} diff --git a/R/update-ace-editor.R b/R/update-ace-editor.R index 63fd972..cbde6f9 100644 --- a/R/update-ace-editor.R +++ b/R/update-ace-editor.R @@ -1,15 +1,15 @@ #' Update Ace Editor -#' +#' #' Update the styling or mode of an aceEditor component. #' @param session The Shiny session to whom the editor belongs #' @param editorId The ID associated with this element #' @param value The initial text to be contained in the editor. #' @param mode The Ace \code{mode} to be used by the editor. The \code{mode} -#' in Ace is often the programming or markup language that you're using and +#' in Ace is often the programming or markup language that you're using and #' determines things like syntax highlighting and code folding. Use the #' \code{\link{getAceModes}} function to enumerate all the modes available. #' @param theme The Ace \code{theme} to be used by the editor. The \code{theme} -#' in Ace determines the styling and coloring of the editor. Use +#' in Ace determines the styling and coloring of the editor. Use #' \code{\link{getAceThemes}} to enumerate all the themes available. #' @param readOnly If set to \code{TRUE}, Ace will disable client-side editing. #' If \code{FALSE} (the default), it will enable editing. @@ -22,34 +22,37 @@ #' @param showInvisibles Show invisible characters (e.g., spaces, tabs, newline characters). #' Default value is FALSE #' @param border Set the \code{border} 'normal', 'alert', or 'flash'. -#' @param autoComplete Enable/Disable code completion. See \code{\link{aceEditor}} +#' @param autoComplete Enable/Disable code completion. See \code{\link{aceEditor}} #' for details. -#' @param autoCompleteList If set to \code{NULL}, exisitng static completions +#' @param autoCompleters List of completers to enable. If set to \code{NULL}, +#' all completers will be disabled. +#' @param autoCompleteList If set to \code{NULL}, exisitng static completions #' list will be unset. See \code{\link{aceEditor}} for details. #' @examples \dontrun{ #' shinyServer(function(input, output, session) { #' observe({ -#' updateAceEditor(session, "myEditor", "Updated text for editor here", +#' updateAceEditor(session, "myEditor", "Updated text for editor here", #' mode="r", theme="ambiance") #' }) #' } -#' } +#' } #' @author Jeff Allen \email{jeff@@trestletech.com} #' @export updateAceEditor <- function( session, editorId, value, theme, readOnly, mode, fontSize, wordWrap, useSoftTabs, tabSize, showInvisibles, border = c("normal", "alert", "flash"), - autoComplete = c("disabled", "enabled", "live"), + autoComplete = c("disabled", "enabled", "live"), + autoCompleters = c("snippet", "text", "keyword", "static", "rlang"), autoCompleteList = NULL ) { - + if (missing(session) || missing(editorId)) { stop("Must provide both a session and an editorId to update Ace.") } - + theList <- list(id = editorId) - + if (!missing(value)) { theList["value"] <- value } @@ -85,10 +88,17 @@ updateAceEditor <- function( autoComplete <- match.arg(autoComplete) theList["autoComplete"] <- autoComplete } + # TODO: add autoCompleters to aceEditor constructors + if (!missing(autoCompleters)) { + if (!is.null(autoCompleters)) { + autoCompleters <- match.arg(autoCompleters, several.ok = TRUE) + } + theList <- c(theList, list(autoCompleters = autoCompleters)) + } if (!missing(autoCompleteList)) { #NULL can only be inserted via c() theList <- c(theList, list(autoCompleteList = autoCompleteList)) } - + session$sendCustomMessage("shinyAce", theList) -} \ No newline at end of file +} diff --git a/inst/examples/06-autocomplete/server.R b/inst/examples/06-autocomplete/server.R index 53c8733..40699c1 100644 --- a/inst/examples/06-autocomplete/server.R +++ b/inst/examples/06-autocomplete/server.R @@ -44,6 +44,23 @@ shinyServer(function(input, output, session) { } }) + #Enable/Disable completers + observe({ + completers <- c() + if (input$enableLocalCompletion) { + completers <- c(completers, "text") + } + if (input$enableNameCompletion) { + completers <- c(completers, "static") + } + if (input$enableRCompletion) { + completers <- c(completers, "rlang") + } + + updateAceEditor(session, "mutate", autoCompleters = completers) + updateAceEditor(session, "plot", autoCompleters = completers) + }) + output$plot <- renderPlot({ input$eval diff --git a/inst/examples/06-autocomplete/ui.R b/inst/examples/06-autocomplete/ui.R index c3c4b94..bce1e2f 100644 --- a/inst/examples/06-autocomplete/ui.R +++ b/inst/examples/06-autocomplete/ui.R @@ -24,7 +24,8 @@ shinyUI(fluidPage( wellPanel( checkboxInput("enableLiveCompletion", "Live auto completion", TRUE), checkboxInput("enableNameCompletion", list("Dataset column names completion in", tags$i("mutate")), TRUE), - checkboxInput("enableRCompletion", "R code completion", TRUE) + checkboxInput("enableRCompletion", "R code completion", TRUE), + checkboxInput("enableLocalCompletion", "Local text completion", TRUE) ) ), textOutput("error") diff --git a/inst/www/shinyAce.js b/inst/www/shinyAce.js index 92695d7..6f4f4c5 100644 --- a/inst/www/shinyAce.js +++ b/inst/www/shinyAce.js @@ -59,10 +59,13 @@ var rlangCompleter = { getCompletions: function(editor, session, pos, prefix, callback) { //if (prefix.length === 0) { callback(null, []); return } var inputId = editor.container.id; - Shiny.onInputChange(inputId + '_hint', { + // TODO: consider dropping onInputChange hook when completer is disabled for performance + Shiny.onInputChange(inputId + '_shinyAce_hint', { + // TODO: add an option to disable full document passing for performance + document: session.getValue(), linebuffer: session.getLine(pos.row), - cursorPosition: pos.column, - // nonce causes autcomplement event to trigger + cursorPosition: pos, + // nonce causes autocomplete event to trigger // on R side even if Ctrl-Space is pressed twice // with the same linebuffer and cursorPosition nonce: Math.random() @@ -70,10 +73,9 @@ var rlangCompleter = { //store callback for dynamic completion $('#' + inputId).data('autoCompleteCallback', callback); } + // TODO: add option to include optional getDocTooltip for suggestion context }; langTools.addCompleter(rlangCompleter); -})(); - Shiny.addCustomMessageHandler('shinyAce', function(data) { var id = data.id; @@ -117,6 +119,35 @@ Shiny.addCustomMessageHandler('shinyAce', function(data) { editor.setOption('enableBasicAutocompletion', value !== 'disabled'); } + if (data.hasOwnProperty('autoCompleters')) { + var completers = data.autoCompleters; + editor.completers = []; + if (completers) { + if (!Array.isArray(completers)) { + completers = [completers]; + } + completers.forEach(function(completer) { + switch (completer) { + case 'snippet': + editor.completers.push(langTools.snippetCompleter); + break; + case 'text': + editor.completers.push(langTools.textCompleter); + break; + case 'keyword': + editor.completers.push(langTools.keyWordCompleter); + break; + case 'static': + editor.completers.push(staticCompleter); + break; + case 'rlang': + editor.completers.push(rlangCompleter); + break; + } + }); + } + } + if (data.tabSize) { editor.setOption('tabSize', data.tabSize); } @@ -138,11 +169,8 @@ Shiny.addCustomMessageHandler('shinyAce', function(data) { } if (data.codeCompletions) { - var words = data.codeCompletions.split(/[ ,]+/).map(function(e) { - return {name: e, value: e, meta: 'R'}; - }); var callback = $el.data('autoCompleteCallback'); - if(callback !== undefined) callback(null, words); + if(callback !== undefined) callback(null, data.codeCompletions); } }); @@ -152,4 +180,6 @@ var toggle_search_replace = ace.require("ace/ext/searchbox").SearchBox.prototype var isReplace = sb.isReplace = !sb.isReplace; sb.replaceBox.style.display = isReplace ? "" : "none"; sb[isReplace ? "replaceInput" : "searchInput"].focus(); -}) +}); + +})(); \ No newline at end of file diff --git a/man/updateAceEditor.Rd b/man/updateAceEditor.Rd index 0eb0d70..b8040b8 100644 --- a/man/updateAceEditor.Rd +++ b/man/updateAceEditor.Rd @@ -7,6 +7,7 @@ updateAceEditor(session, editorId, value, theme, readOnly, mode, fontSize, wordWrap, useSoftTabs, tabSize, showInvisibles, border = c("normal", "alert", "flash"), autoComplete = c("disabled", "enabled", "live"), + autoCompleters = c("snippet", "text", "keyword", "static", "rlang"), autoCompleteList = NULL) } \arguments{ @@ -17,14 +18,14 @@ updateAceEditor(session, editorId, value, theme, readOnly, mode, fontSize, \item{value}{The initial text to be contained in the editor.} \item{theme}{The Ace \code{theme} to be used by the editor. The \code{theme} -in Ace determines the styling and coloring of the editor. Use +in Ace determines the styling and coloring of the editor. Use \code{\link{getAceThemes}} to enumerate all the themes available.} \item{readOnly}{If set to \code{TRUE}, Ace will disable client-side editing. If \code{FALSE} (the default), it will enable editing.} \item{mode}{The Ace \code{mode} to be used by the editor. The \code{mode} -in Ace is often the programming or markup language that you're using and +in Ace is often the programming or markup language that you're using and determines things like syntax highlighting and code folding. Use the \code{\link{getAceModes}} function to enumerate all the modes available.} @@ -43,10 +44,13 @@ Default value is FALSE} \item{border}{Set the \code{border} 'normal', 'alert', or 'flash'.} -\item{autoComplete}{Enable/Disable code completion. See \code{\link{aceEditor}} +\item{autoComplete}{Enable/Disable code completion. See \code{\link{aceEditor}} for details.} -\item{autoCompleteList}{If set to \code{NULL}, exisitng static completions +\item{autoCompleters}{List of completers to enable. If set to \code{NULL}, +all completers will be disabled.} + +\item{autoCompleteList}{If set to \code{NULL}, exisitng static completions list will be unset. See \code{\link{aceEditor}} for details.} } \description{ @@ -56,11 +60,11 @@ Update the styling or mode of an aceEditor component. \dontrun{ shinyServer(function(input, output, session) { observe({ - updateAceEditor(session, "myEditor", "Updated text for editor here", + updateAceEditor(session, "myEditor", "Updated text for editor here", mode="r", theme="ambiance") }) } -} +} } \author{ Jeff Allen \email{jeff@trestletech.com}