Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP][Completion] Configurable Completers #53

Merged
merged 2 commits into from
Apr 6, 2018
Merged
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
30 changes: 18 additions & 12 deletions R/ace-autocomplete.R
Original file line number Diff line number Diff line change
@@ -1,33 +1,39 @@
#' 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(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)
})
}
}
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
50 changes: 40 additions & 10 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;
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()
});
//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.