Эта идея пришла мне в голову, чтобы помочь с отладкой блестящих приложений R, с которыми вы можете взаимодействовать во время их работы. Вы можете вводить что угодно в консоль R, пока блестящее приложение работает.

Вот я и подумал, а можно ли использовать интерактивную оболочку внутри браузера, написанного на JavaScript? этот пост является результатом того, что я написал эту интерактивную оболочку.

Во-первых, когда мы хотим написать интерпретатор языка R, нам нужна функция eval, которая вернет результат в виде строки. Эта функция выглядит так.

r.eval <- function(code) {
  paste(capture.output(eval(parse(text = code))), collapse = "\n")
}

Теперь внутри JavaScript нам нужно отправить строку, которую мы хотим оценить, из браузера в процесс R.

var index = 0;
function exec(str) {
  var id = index++;
  Shiny.onInputChange('__EVAL__', {id: id, value: str});
}

Мы используем очень необычное имя события, вы должны выбрать другое, чтобы вас не взломали, если кто-то попытается использовать эту строку в вашем общедоступном приложении. Другое решение — добавить этот код только для определенных пользователей (например, разработчиков или администраторов).

Мы улучшим эту функцию позже, это будет функция, которая возвращает промис с результатом из R.

Теперь нам нужно написатьObservEvent (внутри серверной функции), чтобы получить значение из JavaScript и оценить значение в R и отправить результат обратно в браузер.

observeEvent(input[["__EVAL__"]], {
  data <- input[["__EVAL__"]]
  tryCatch({
    payload <- list(id = data$id, result = r.eval(data$value))
    session$sendCustomMessage("__EVAL__", payload)
  }, error = function(cond) {
    error <- paste(capture.output(traceback(cond)), collapse = "\n")
    payload <-  list(id = data$id, error = error) 
    session$sendCustomMessage("__EVAL__", payload)
  })
})

Внутри R у нас есть все. Далее нам нужен лучший код JavaScript. Если вам не нужен этот код и вам нужно решение, в конце есть закладка, которую можно использовать для создания терминала без написания JavaScript.

Для тех, кто хочет следовать, нам нужно написать лучшую функцию exec. Вот:

var exec = (function() {
  var handlers = [];
  if (typeof Shiny !== 'undefined') {
    Shiny.addCustomMessageHandler("__EVAL__", function(data) {
      handlers.forEach(function(handler) {
        handler(data);
      });
    });
  }
  var index = 0;
  function exec(str, callback) {
    if (!str.trim()) { // no data
      return Promise.resolve();
    }
    return new Promise(function(resolve, reject) {
      var id = index++;
      
      handlers.push(function handler(data) {
        if (data.id === id) {
          if (data.error) {
            reject(data.error);
          } else {
            resolve(data.result);
          }
        }
        handlers = handlers.filter(function(f) {
          return f !== handler;
        });
      });
      Shiny.onInputChange('__EVAL__', {id: id, value: str});
    });
  }
  return exec;
})();

Код использует синтаксис ES5, не стесняйтесь реорганизовать его для использования ES6 (ES2015) или любой новой версии JavaScript.

Теперь вы можете в консоли инструментов разработчика JavaScript оценить R-код и получить ответ:

await exec("10 + 10")
[1] 20

await верхнего уровня может работать только в инструментах разработчика. если это не работает, вы можете использовать:

exec("10 + 10").then(result => console.log(result));

Теперь все, что нужно сделать, это написать красиво выглядящий терминал, чтобы вы могли вводить команды и получать ответ из блестящего приложения. Я буду использовать свою библиотеку JavaScript jQuery Terminal.

Сначала нам нужно подключить библиотеки:

<script src="https://unpkg.com/jquery.terminal/js/jquery.terminal.min.js"></script>
<link href="https://unpkg.com/jquery.terminal/css/jquery.terminal.min.css" rel="stylesheet"/>

Мы также можем захотеть иметь подсветку синтаксиса R, чтобы он выглядел лучше. Нам нужен этот html для включения соответствующих библиотек:

<link href="https://unpkg.com/prismjs/themes/prism-coy.css" rel="stylesheet"/>
<script src="https://unpkg.com/prismjs/prism.js"></script>
<script src="https://unpkg.com/jquery.terminal/js/prism.js"></script>
<script src="https://unpkg.com/prismjs/components/prism-r.min.js"></script>

После добавления зависимостей нам нужно создать терминал. Мы хотим, чтобы терминал располагался внизу экрана, поэтому сначала мы пишем базовую html-структуру.

<div class="shell-wrapper">
  <span class="shell-destroy">[x]</span>
  <div id="r-term"></div>
</div>

Теперь нам нужно придать стиль:

.terminal {
  --size: 1.2;
  height: 100%;
}
.shell-wrapper {
  position: fixed;
  z-index: 99999;
  bottom: 0;
  left: 0;
  right: 0;
  height: 150px;
}
.shell-destroy {
  position: absolute;
  right: 10px;
  color: #ccc;
  top: 10px;
  z-index: 10;
  cursor: pointer;
  font-family: monospace;
}

Теперь заключительная часть — создание терминала с интерактивной оболочкой R.

var term = $('#r-term').terminal(function(cmd, term) {
  if (!cmd.trim()) {
    // we ignore empty command but exec will also handle that
    return;
  }
  return exec(cmd).then(function(result) {
    result = result.trim();
    if (result) {
      term.echo($.terminal.escape_brackets(result));
    }
  }).catch(function(e) {
    term.error(e);
  });
}, {
  greetings: 'R console\n'
});

И теперь у вас есть настоящий терминал в браузере, который дает вам настоящую интерактивную сессию R в JavaScript.

Вы также можете использовать небольшой плагин под названием тильда вместо приведенного выше кода, вам нужно только использовать тильду вместо терминала. И у вас будет quake-подобная консоль в вашем приложении, которая открывается, когда вы нажимаете tidla ~ на клавиатуре.

Tidla можно найти на GitHub как один из примеров jQuery Terminal. Добавление Quake как консоль R остается читателю в качестве упражнения.

И как я и обещал вот ссылка на букмарклет, он не очень красивый, но в нем есть все из этого поста. Он загружает все файлы, и вы можете щелкнуть его несколько раз, чтобы уничтожить предыдущий терминал.

ОБНОВЛЕНИЕ:

Пока я интегрировал этот отладчик в свое блестящее приложение, я сделал несколько улучшений, которыми хочу поделиться.

Я создал две функции debugger.init() и debugger(). Init следует вызывать внутри серверной функции, поскольку для этого требуются входные данные и сеанс. И функцию отладчика можно использовать для изменения контекста, когда интерпретатор оценивает выражения, как и browser(), но приложение все еще работает.

А вот и код, который можно поместить в пакет.

#' To use Debugger you need to call debugger.init(input, session)
#' inside shiny server function. Then you need complementary JS code
#' that is avaiable as bookmarklet. Then in any context you can
#' call debugger function to change the context (it's optional).
#' If you call that function in constructor  you will be able 
#' to access self and private properties of the object.
#' eval to string function that work the same as R REPL
#' @param code - string to evaluate
#' @param env - enviroment in which to evaluate the code
#' @export
r.eval <- function(code, env = parent.frame()) {
  paste(
    capture.output(eval(parse(text = code), envir = env)),
    collapse = "\n"
  )
}
#' debugger environment
.global <- list2env(list(env = NULL))
#' function to use instead of browser() and put context of debugger in this place
#' @export
debugger <- function() {
   .global$env <- parent.frame()
}
#' Initialization of the debugger
#' @param input - shiny input
#' @param session - shiny session
#' @export
debugger.init <- function(input, session, env = parent.frame()) {
  .global$env <- env
  shiny::observeEvent(input[["__EVAL__"]], {
    data <- input[["__EVAL__"]]
    tryCatch({
      payload <- list(
        id = data$id,
        result = r.eval(data$value, env = .global$env)
      )
      session$sendCustomMessage("__EVAL__", payload)
    }, error = function(cond) {
      error <- paste(
          capture.output(traceback(cond)),
          collapse = "\n"
      )
      payload <-  list(id = data$id, error = error)
      session$sendCustomMessage("__EVAL__", payload)
    })
  })
}