Skip to content

Commit

Permalink
Close #15: Replace CodeMirror w/ Monaco (#16)
Browse files Browse the repository at this point in the history
* fix #15: CodeMirror -> Monaco

* use Monaco v0.37.1

* Monaco: v0.37.1 -> v0.31.0

* make startup message more accessible

* Fix canvas issues

* Add a way for monaco to dynamically resize

Implementation based on: microsoft/monaco-editor#794 (comment)

* Disable scrolling past end of cell

* Disable the minimap

* Cleanup spaces

* Write a for more comments throughout the file

* Improve the editor space

* Lowercase webR

* Slight wording tweaks

* lint demo qmd file

---------

Co-authored-by: James J Balamuta <[email protected]>
  • Loading branch information
jooyoungseo and coatless authored May 2, 2023
1 parent 7c605ee commit 2cc9dcb
Show file tree
Hide file tree
Showing 4 changed files with 130 additions and 38 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ quarto add coatless/quarto-webr

## Usage

For each document, place the the `webr` filter in the document's header:
For each document, place the `webr` filter in the document's header:

```yaml
filters:
Expand Down Expand Up @@ -97,7 +97,7 @@ webr::install("ggplot2")
The `quarto-webr` extension supports specifying the following `WebROptions` options:

- `home-dir`: The WebAssembly user’s home directory and initial working directory ([`Documentation`](https://docs.r-wasm.org/webr/latest/api/js/interfaces/WebR.WebROptions.html#homedir)). Default: `'/home/web_user'`.
- `base-url`: The base URL used for downloading R WebAssembly binaries. ([`Documentation`](https://docs.r-wasm.org/webr/latest/api/js/interfaces/WebR.WebROptions.html#baseurl)). Default: `'https://webr.r-wasm.org/[version]/'`.
- `base-url`: The base URL used for downloading R WebAssembly binaries ([`Documentation`](https://docs.r-wasm.org/webr/latest/api/js/interfaces/WebR.WebROptions.html#baseurl)). Default: `'https://webr.r-wasm.org/[version]/'`.
- `service-worker-url`: The base URL from where to load JavaScript worker scripts when loading webR with the ServiceWorker communication channel mode ([`Documentation`](https://docs.r-wasm.org/webr/latest/api/js/interfaces/WebR.WebROptions.html#serviceworkerurl)). Default: `''`.

The extension also has native options for:
Expand Down
119 changes: 100 additions & 19 deletions _extensions/webr/webr-editor.html
Original file line number Diff line number Diff line change
@@ -1,74 +1,155 @@
<button class="btn btn-default btn-webr" disabled type="button" id="webr-run-button-{{WEBRCOUNTER}}">Loading webR...</button>
<button class="btn btn-default btn-webr" disabled type="button" id="webr-run-button-{{WEBRCOUNTER}}">Loading
webR...</button>
<div id="webr-editor-{{WEBRCOUNTER}}"></div>
<div id="webr-code-output-{{WEBRCOUNTER}}"><pre style="visibility: hidden"></pre></div>
<div id="webr-code-output-{{WEBRCOUNTER}}" aria-live="assertive">
<pre style="visibility: hidden"></pre>
</div>
<script type="module">
// Retrieve webR code cell information
const runButton = document.getElementById("webr-run-button-{{WEBRCOUNTER}}");
const outputDiv = document.getElementById("webr-code-output-{{WEBRCOUNTER}}");
const editorDiv = document.getElementById("webr-editor-{{WEBRCOUNTER}}");

const editor = CodeMirror((elt) => {
elt.style.border = "1px solid #eee";
elt.style.height = "auto";
editorDiv.append(elt);
},{
value: `{{WEBRCODE}}`,
lineNumbers: true,
mode: "r",
theme: "light default",
viewportMargin: Infinity,
// Add a light grey outline around the code editor
editorDiv.style.border = "1px solid #eee";

// Load the Monaco Editor and create an instance
let editor;
require(['vs/editor/editor.main'], function () {
editor = monaco.editor.create(editorDiv, {
value: `{{WEBRCODE}}`,
language: 'r',
theme: 'vs-light',
automaticLayout: true, // TODO: Could be problematic for slide decks
scrollBeyondLastLine: false,
minimap: {
enabled: false
},
fontSize: '17.5rem', // Bootstrap is 1 rem
renderLineHighlight: "none", // Disable current line highlighting
hideCursorInOverviewRuler: true // Remove cursor indictor in right hand side scroll bar
});

// Add a keydown event listener for Shift+Enter using the addCommand method
editor.addCommand(monaco.KeyMod.Shift | monaco.KeyCode.Enter, function () {
// Code to run when Shift+Enter is pressed
executeCode(editor.getValue());
});

// Add a keydown event listener for Ctrl+Enter to run selected code
editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter, function () {
// Get the selected text from the editor
const selectedText = editor.getModel().getValueInRange(editor.getSelection());
// Code to run when Ctrl+Enter is pressed (run selected code)
executeCode(selectedText);
});

// Dynamically modify the height of the editor window if new lines are added.
let ignoreEvent = false;
const updateHeight = () => {
const contentHeight = editor.getContentHeight();
// We're avoiding a width change
//editorDiv.style.width = `${width}px`;
editorDiv.style.height = `${contentHeight}px`;
try {
ignoreEvent = true;

// The key to resizing is this call
editor.layout();
} finally {
ignoreEvent = false;
}
};

// Register an on change event for when new code is added to the editor window
editor.onDidContentSizeChange(updateHeight);

// Manually re-update height to account for the content we inserted into the call
updateHeight();
});

runButton.onclick = async () => {
// Function to execute the code (accepts code as an argument)
async function executeCode(codeToRun) {
// Disable run button for code cell active
runButton.disabled = true;

// Create a canvas variable for graphics
let canvas = undefined;

// Initialize webR
await globalThis.webR.init();

// Setup a webR canvas
await webR.evalRVoid("canvas(width={{WIDTH}}, height={{HEIGHT}})");
const result = await webRCodeShelter.captureR(editor.getValue(), {

// Capture output data from evaluating the code
const result = await webRCodeShelter.captureR(codeToRun, {
withAutoprint: true,
captureStreams: true,
captureConditions: false,
env: webR.objs.emptyEnv,
});

// Start attempting to parse the result data
try {

// Stop creating images
await webR.evalRVoid("dev.off()");

// Merge output streams of STDOUT and STDErr (messages and errors are combined.)
const out = result.output.filter(
evt => evt.type == "stdout" || evt.type == "stderr"
).map((evt) => evt.data).join("\n");

// Clean the state
const msgs = await webR.flush();

// Output each image stored
msgs.forEach(msg => {
if (msg.type === "canvasExec"){
if (msg.type === "canvasExec") {
if (!canvas) {
canvas = document.createElement("canvas");
canvas.setAttribute("width", 2 * {{WIDTH}});
canvas.setAttribute("height", 2 * {{HEIGHT}});
canvas.style.width="700px";
canvas.style.display="block";
canvas.style.margin="auto";
canvas.style.width = "700px";
canvas.style.display = "block";
canvas.style.margin = "auto";
}
Function(`this.getContext("2d").${msg.data}`).bind(canvas)();
}
});

// Nullify the outputDiv of content
outputDiv.innerHTML = "";

// Design an output object for messages
const pre = document.createElement("pre");
if (/\S/.test(out)) {
// Display results as text
const code = document.createElement("code");
code.innerText = out;
pre.appendChild(code);
} else {
// If nothing is present, hide the element.
pre.style.visibility = "hidden";
}
outputDiv.appendChild(pre);

// Place the graphics on the canvas
if (canvas) {
const p = document.createElement("p");
p.appendChild(canvas);
outputDiv.appendChild(p);
}
} finally {
// Clean up the remaining code
webRCodeShelter.purge();
runButton.disabled = false;
}
}
</script>

// Add a click event listener to the run button
runButton.onclick = function () {
executeCode(editor.getValue());
};
</script>
22 changes: 16 additions & 6 deletions _extensions/webr/webr-init.html
Original file line number Diff line number Diff line change
@@ -1,18 +1,26 @@
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/6.65.7/codemirror.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/min/vs/editor/editor.main.css" />
<style>
.CodeMirror pre {
.monaco-editor pre {
background-color: unset !important;
}

.btn-webr {
background-color: #EEEEEE;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
</style>
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/6.65.7/codemirror.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/6.65.7/mode/r/r.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/min/vs/loader.js"></script>
<script type="module">

// Configure the Monaco Editor's loader
require.config({
paths: {
'vs': 'https://cdn.jsdelivr.net/npm/[email protected]/min/vs'
}
});


// Start a timer
const initializeWebRTimerStart = performance.now();

Expand Down Expand Up @@ -47,6 +55,8 @@
var startupMessageWebR = document.createElement("p");
startupMessageWebR.innerText = "🟡 Loading...";
startupMessageWebR.setAttribute("id", "startup");
// Add `aria-live` to auto-announce the startup status to screen readers
startupMessageWebR.setAttribute("aria-live", "assertive");

// Put everything together
secondInnerDivContents.appendChild(startupMessageWebR);
Expand All @@ -70,7 +80,7 @@

// Retrieve the webr.mjs
import { WebR } from "https://webr.r-wasm.org/v0.1.1/webr.mjs";

// Populate WebR options with defaults or new values based on
// webr meta
globalThis.webR = new WebR({
Expand Down Expand Up @@ -106,4 +116,4 @@
// If initialized, switch to a green light
startupMessageWebR.innerText = "🟢 Ready!"
}
</script>
</script>
23 changes: 12 additions & 11 deletions webr-demo.qmd
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
title: "WebR-enabled code cells"
title: "webR-enabled code cells"
format: html
engine: knitr
filters:
Expand All @@ -8,18 +8,18 @@ filters:

## Demo

WebR-enabled code cell are established by using `{webr-r}` in a Quarto HTML document.
webR-enabled code cell are established by using `{webr-r}` in a Quarto HTML document.

```{webr-r}
1 + 1
1 + 1
```

## Sample cases

### Fit a linear regression model

```{webr-r}
fit = lm(mpg ~ am, data = mtcars)
fit <- lm(mpg ~ am, data = mtcars)
summary(fit)
```

Expand All @@ -38,8 +38,9 @@ You can view what packages are available for webR by either executing the follow

```{webr-r}
available.packages(
repos="https://repo.r-wasm.org/",
type="source")[,c("Package", "Version")]
repos = "https://repo.r-wasm.org/",
type = "source"
)[, c("Package", "Version")]
```

Or, by navigating to the WebR repository:
Expand All @@ -61,15 +62,15 @@ Once `ggplot2` is loaded, then use the package as normal.
```{webr-r}
library(ggplot2)
p = ggplot(mpg, aes(class, hwy))
p <- ggplot(mpg, aes(class, hwy))
p + geom_boxplot()
```

### Define variables and re-use them in later cells

```{webr-r}
name = "James"
age = 42
name <- "James"
age <- 42
```


Expand All @@ -81,7 +82,7 @@ message(name, " is ", age, " years old!")
### Escape characters in a string

```{webr-r}
seven_seas = "Ahoy, matey!\nLet's set sail for adventure!\n"
seven_seas <- "Ahoy, matey!\nLet's set sail for adventure!\n"
seven_seas
```

Expand Down Expand Up @@ -109,4 +110,4 @@ add_one(2)

```{r}
message("Hello!")
```
```

0 comments on commit 2cc9dcb

Please sign in to comment.