Skip to content

Commit

Permalink
[Completion] Configurable Completers
Browse files Browse the repository at this point in the history
Improve configurability of completers:

1. Completers can be selectively enabled and disabled. `snippet` and `keywords` completers source completions from Ace's language mode definition. `text` completer completes with local words.
1. R language completer now receives full text in addition to the current line buffer due to full text lags due to debouncing.
1. R language completer now provides full completion object such that fields can be customized: name, value, description and meta.
  • Loading branch information
Forest Fang committed Apr 5, 2018
1 parent d04ae51 commit 208203d
Show file tree
Hide file tree
Showing 6 changed files with 109 additions and 40 deletions.
29 changes: 18 additions & 11 deletions R/ace-autocomplete.R
Original file line number Diff line number Diff line change
@@ -1,33 +1,40 @@
#' 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_<editorId>_hint}
#' where <editorId> is the \code{aceEditor} id. The input contains
#' \itemize{
#' \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("shinyAce_", inputId, "_hint")]]
if(is.null(value)) return(NULL)
if (is.null(value)) return(NULL)

utilEnv <- environment(utils::alarm)
w32 <- get(".win32consoleCompletion", utilEnv)

comps <- list(id = 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)
})
}
}
36 changes: 23 additions & 13 deletions R/update-ace-editor.R
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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
}
Expand Down Expand Up @@ -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)
}
}
17 changes: 17 additions & 0 deletions inst/examples/06-autocomplete/server.R
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
3 changes: 2 additions & 1 deletion inst/examples/06-autocomplete/ui.R
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
48 changes: 39 additions & 9 deletions inst/www/shinyAce.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,21 +59,23 @@ var rlangCompleter = {
getCompletions: function(editor, session, pos, prefix, callback) {
//if (prefix.length === 0) { callback(null, []); return }
var inputId = editor.container.id;
// TODO: consider dropping onInputChange hook when completer is disabled for performance
Shiny.onInputChange('shinyAce_' + inputId + '_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()
});
//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;
Expand Down Expand Up @@ -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);
}
Expand All @@ -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);
}
});

Expand All @@ -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();
})
});

})();
16 changes: 10 additions & 6 deletions man/updateAceEditor.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 208203d

Please sign in to comment.