Skip to content

Commit

Permalink
Port eval-region to squint (#44)
Browse files Browse the repository at this point in the history
Also export eval-region extension as module available at

```
import * as evalRegion from '@nextjournal/clojure-mode/extensions/eval-region'
```
  • Loading branch information
borkdude authored Nov 22, 2023
1 parent 1199223 commit 0bcc17f
Show file tree
Hide file tree
Showing 11 changed files with 187 additions and 264 deletions.
5 changes: 5 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,11 @@ jobs:
run: |
yarn build
- name: 🧶 Squint Build
run: |
yarn squint compile
yarn vite:build
- name: 📠 Copy static build to bucket under SHA
run: gsutil cp -r public gs://nextjournal-snapshots/clojure-mode/build/${{ github.sha }}

Expand Down
18 changes: 11 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
{ "name": "@nextjournal/clojure-mode",
"files": ["dist"],
{
"name": "@nextjournal/clojure-mode",
"files": [
"dist"
],
"version": "0.2.0",
"license": "EPL-2.0",
"repository": {
Expand All @@ -20,9 +23,8 @@
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.0.0",
"@nextjournal/lezer-clojure": "1.0.0",
"squint-cljs": "0.3.36",
"w3c-keyname": "^2.2.4",
"squint-macros": "https://github.com/squint-cljs/squint-macros"
"squint-cljs": "0.4.58",
"w3c-keyname": "^2.2.4"
},
"comments": {
"to run squint as a local dependency:": "bb yarn-install:squint-dev"
Expand Down Expand Up @@ -52,9 +54,11 @@
"react-dom": "^17.0.2",
"rollup-plugin-analyzer": "^4.0.0",
"shadow-cljs": "2.19.5",
"vite": "^4.4.9"
"vite": "^4.4.9",
"@squint-cljs/macros": "0.1.0"
},
"exports": {
".": "./dist/nextjournal/clojure_mode.mjs"
".": "./dist/nextjournal/clojure_mode.mjs",
"./extensions/eval-region": "./dist/nextjournal/clojure_mode/extensions/eval_region.mjs"
}
}
22 changes: 15 additions & 7 deletions public/squint/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,14 @@
}
}
</style>
<script type="importmap">
{
"imports": {
"squint-cljs/core.js": "https://unpkg.com/[email protected]/core.js"
}
}
</script>

</head>
<body>
<div class="landing-page pt-10">
Expand Down Expand Up @@ -88,12 +96,12 @@ <h2 id="try-it" class="mt-0 mb-12 text-center text-3xl font-bold">
</h2>
<div class="flex flex-col-reverse md:flex-row">
<div class="md:w-1/2 flex-shrink-0 md:px-6 mt-12 md:mt-0">
<h3 class="text-center sans-serif font-bold text-lg mt-0 mb-1">Try evaluating any of these forms with <span class="kbd alt font-normal">Alt</span> <span class="font-normal">+</span> <span class="kbd font-normal"></span> !</h3>
<h3 class="text-center sans-serif font-bold text-lg mt-0 mb-1">Try evaluating any of these forms with <span class="kbd mod font-normal">Mod</span> <span class="font-normal">+</span> <span class="kbd font-normal"></span> !</h3>
<p class="sans-serif text-sm text-center mb-6 mt-0">
In-browser eval is powered by <a href="https://github.com/borkdude/sci">Sci</a>.
In-browser eval is powered by <a href="https://github.com/squint-cljs/squint">Squint</a>.
</p>
<div id="editor" class="rounded-md mb-0 text-sm monospace overflow-auto relative border shadow-lg bg-white">
</div>
<div id="editor" class="rounded-md mb-0 text-sm monospace overflow-auto relative border shadow-lg bg-white"></div>
<div id="result" class="mt-3.mv-4.pl-6" style="white-space: pre-wrap; font-family: var(--code-font)"></div>
</div>
<div class="md:w-1/2 flex-shrink-0 md:px-6 sans-serif">
<ul class="text-lg">
Expand Down Expand Up @@ -162,23 +170,23 @@ <h3 class="text-center sans-serif font-bold text-lg mt-0 mb-1">Try evaluating an
At Cursor
</td>
<td class="py-1 text-right">
<span class="kbd alt">Alt</span> + <span class="kbd"></span>
<span class="kbd mod">Mod</span> + <span class="kbd"></span>
</td>
</tr>
<tr class="border-t">
<td class="py-1 pr-12">
Top-level form
</td>
<td class="py-1 text-right">
<span class="kbd alt">Alt</span> + <span class="kbd"></span> + <span class="kbd"></span>
<span class="kbd alt">Mod</span> + <span class="kbd"></span> + <span class="kbd"></span>
</td>
</tr>
<tr class="border-t">
<td class="py-1 pr-12">
Cell
</td>
<td class="py-1 text-right">
<span class="kbd mod">Mod</span> + <span class="kbd"></span>
<span class="kbd alt">Alt</span> + <span class="kbd"></span>
</td>
</tr>
</tbody>
Expand Down
114 changes: 110 additions & 4 deletions public/squint/js/demo.mjs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { default_extensions, complete_keymap } from '@nextjournal/clojure-mode';
import { extension as eval_ext, cursor_node_string, top_level_string } from '@nextjournal/clojure-mode/extensions/eval-region';
import { EditorView, drawSelection, keymap } from '@codemirror/view';
import { EditorState } from '@codemirror/state';
import { syntaxHighlighting, defaultHighlightStyle, foldGutter } from '@codemirror/language';
import { compileString } from 'squint-cljs';

let theme = EditorView.theme({
".cm-content": {whitespace: "pre-wrap",
Expand All @@ -23,14 +25,79 @@ let theme = EditorView.theme({
"&.cm-focused .cm-cursor": {visibility: "visible"}
});

let evalCode = async function (code) {
let js = compileString(`(do ${code})`, {repl: true,
context: 'return',
"elide-exports": true})
let result;
try {
result = {value: await eval(`(async function() { ${js} })()`)};
}
catch (e) {
result = {error: true, ex: e};
}
if (result.error) {
document.getElementById("result").innerText = result.ex;
} else {
document.getElementById("result").innerText = '' + JSONstringify(result.value);
}
}

let evalCell = (opts) => {
let code = opts.state.doc.toString();
evalCode(code);
return true;
}

let evalToplevel = function (opts) {
let state = opts.state;
let code = top_level_string(state);
evalCode(code);
return true;
}

function JSONstringify(json) {
json = JSON.stringify(json, function(key, value) {
if (!value) return value;
if (typeof value === 'string') return value;
if (Array.isArray(value) || value.constructor === Object) return value;
if (value[Symbol.iterator]) {
return [...value];
}
if (typeof value === 'object') {
return `#object[${value.constructor.name}]`;
} else {
return value;
}
});
return json;
}

let evalAtCursor = function (opts) {
let state = opts.state;
let code = cursor_node_string(state);
evalCode(code);
return true;
}

let squintExtension = ( opts ) => {
return keymap.of([{key: "Alt-Enter", run: evalCell},
{key: opts.modifier + "-Enter",
run: evalAtCursor,
shift: evalToplevel
}])}


let extensions = [ theme, foldGutter(),
syntaxHighlighting(defaultHighlightStyle),
drawSelection(),
keymap.of(complete_keymap),
...default_extensions
...default_extensions,
eval_ext({modifier: "Meta"}),
squintExtension({modifier: "Meta"})
];

let state = EditorState.create( {doc: `(comment
let doc = `(comment
(fizz-buzz 1)
(fizz-buzz 3)
(fizz-buzz 5)
Expand All @@ -43,9 +110,48 @@ let state = EditorState.create( {doc: `(comment
15 "fizzbuzz"
3 "fizz"
5 "buzz"
n))`,
n))
(require '["https://esm.sh/[email protected]$default" :as confetti])
(do
(js-await (confetti))
(+ 1 2 3))
` ;

evalCode(doc);

let state = EditorState.create( {doc: doc,
extensions: extensions });

let editorElt = document.querySelector('#editor');
let editor = new EditorView({state: state,
parent: editorElt,
extensions: extensions });
extensions: extensions })

let keys = {"ArrowUp": "↑",
"ArrowDown": "↓",
"ArrowRight": "→",
"ArrowLeft": "←",
"Mod": "Ctrl"}

let macKeys = {"Alt": "⌥",
"Shift": "⇧",
"Enter": "⏎",
"Ctrl": "⌃",
"Mod": "⌘"}

let mac;

if (/^(Mac)|(iPhone)|(iPad)|(iPod)$/.test(window.navigator.platform.substring(0,3))) {
mac = true;
Object.assign(keys, macKeys);
}

document.querySelectorAll(".mod,.alt,.ctrl").forEach(node => {
let k = node.innerHTML;
let symbol = keys[k];
if (symbol) {
node.innerHTML = symbol;
}
});
3 changes: 2 additions & 1 deletion squint.edn
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
{:paths ["src-shared" "src-squint" "test" "node_modules/squint-macros/src"]
{:paths ["src-shared" "src-squint" "test"
"node_modules/@squint-cljs/macros/src"]
:output-dir "dist"}
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@
["@codemirror/state" :as state :refer [StateEffect StateField]]
["@codemirror/view" :as view :refer [EditorView Decoration keymap]]
["w3c-keyname" :refer [keyName]]
[applied-science.js-interop :as j]
#?@(:squint [] :cljs [[applied-science.js-interop :as j]])
[nextjournal.clojure-mode.util :as u]
[nextjournal.clojure-mode.node :as n]
[clojure.string :as str]))
[clojure.string :as str])
#?(:squint (:require-macros [applied-science.js-interop :as j])))

(defn uppermost-edge-here
"Returns node or its highest ancestor that starts or ends at the cursor position."
Expand All @@ -19,7 +20,8 @@
node))

(defn main-selection [state]
(-> (j/call-in state [:selection :asSingle])
(->
(j/call-in state [:selection :asSingle])
(j/get-in [:ranges 0])))

(defn node-at-cursor
Expand All @@ -45,6 +47,7 @@

;; Modifier field
(defonce modifier-effect (.define StateEffect))

(defonce modifier-field
(.define StateField
(j/lit {:create (constantly {})
Expand All @@ -55,7 +58,7 @@

(defn get-modifier-field [^js state] (.field state modifier-field))

(j/defn set-modifier-field! [^:js {:as view :keys [dispatch state]} value]
(j/defn set-modifier-field! [^:js {:as _view :keys [dispatch]} value]
(dispatch #js{:effects (.of modifier-effect value)
:userEvent "evalregion"}))

Expand Down Expand Up @@ -118,7 +121,7 @@
(when (not= prev next)
(set-modifier-field! view next))
false))
handle-backspace (j/fn [^:js {:as view :keys [state dispatch]}]
handle-backspace (j/fn [^:js {:as _view :keys [state dispatch]}]
(j/let [^:js {:keys [from to]} (current-range state)]
(when (not= from to)
(dispatch (j/lit {:changes {:from from :to to :insert ""}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,8 @@
(u/iter-changed-lines tr
(fn [^js line ^js changes]
(format-line state context (.-from line) (.-text line) (.-number line) changes true)))))))]
(.. tr -startState (update (j/assoc! changes :filter false)))
(do #_(js/console.log :changes changes)
(.. tr -startState (update (j/assoc! changes :filter false))))
tr)))

(defn format [state]
Expand Down
2 changes: 1 addition & 1 deletion src-shared/nextjournal/clojure_mode/node.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@
(defn ^boolean prefix? [n] (prefix-type? (type n)))
(defn ^boolean prefix-edge? [n] (prefix-edge-type? (type n)))
(defn ^boolean prefix-container? [n] (prefix-container-type? (type n)))
(defn ^boolean same-edge? [n] (same-edge-type? (type n)))
(defn ^boolean same-edge? [n ](same-edge-type? (type n)))
(defn ^boolean start-edge? [n]
(start-edge-type? (type n)))
(defn ^boolean end-edge? [n] (end-edge-type? (type n)))
Expand Down
7 changes: 7 additions & 0 deletions src-squint/nextjournal/clojure_mode_tests/macros.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,10 @@
`(do ~@processed))
#?(:clj (throw (IllegalArgumentException. "The number of args doesn't match are's argv."))
:cljs (throw (js/Error "The number of args doesn't match are's argv.")))))

(defmacro is
[expr & _]
(if (and (seq? expr)
(= '= (first expr)))
(list* 'assert.equal (rest expr))
expr))
22 changes: 18 additions & 4 deletions test/nextjournal/clojure_mode_tests.cljc
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
(ns nextjournal.clojure-mode-tests
(:require #?@(:squint []
:cljs [[cljs.test :refer [are testing deftest]]])
:cljs [[cljs.test :refer [are testing deftest is]]])
[nextjournal.clojure-mode :as cm-clojure]
[nextjournal.clojure-mode.util :as util]
[nextjournal.clojure-mode.test-utils :as test-utils]
[nextjournal.clojure-mode.extensions.close-brackets :as close-brackets]
[nextjournal.clojure-mode.commands :as commands]
[nextjournal.clojure-mode.extensions.formatting :as format]
[nextjournal.clojure-mode.extensions.eval-region :as eval-region]
#?@(:squint []
:cljs [[nextjournal.livedoc :as livedoc]])
#?(:squint ["assert" :as assert]))
#?(:squint (:require-macros [nextjournal.clojure-mode-tests.macros :refer [deftest are testing]])))
#?(:squint (:require-macros [nextjournal.clojure-mode-tests.macros :refer [deftest are testing is]])))

(def extensions
cm-clojure/default-extensions
(.concat cm-clojure/default-extensions (eval-region/extension #js {}))
;; optionally test with live grammar
#_
#js[(cm-clojure/syntax live-grammar/parser)
Expand Down Expand Up @@ -322,4 +324,16 @@
"(()|)" "(()\n |)"
"(a |b)" "(a\n |b)"
"(a b|c)" "(a b\n |c)"
)))
))

(deftest eval-region-test
(are [input f expected]
(= (f (test-utils/make-state extensions input)) expected)
"(+ |1 2 3)" eval-region/cursor-node-string "1"
"(+ |(+ 1 2) 2 3)" eval-region/cursor-node-string "(+ 1 2)"
"(+ (+ 1 2)| 2 3)" eval-region/cursor-node-string "(+ 1 2)")
(let [state (test-utils/make-state extensions ";; dude\n|{:a 1}")]
(is (= "{:a 1}" (->> (eval-region/top-level-node state)
(util/range-str state)))))))

#_(prn (eval-region/cursor-node-string (test-utils/make-state extensions "(+ (+ 1 2)| 2 3)")))
Loading

0 comments on commit 0bcc17f

Please sign in to comment.