code.cljs 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631
  1. (ns frontend.extensions.code
  2. (:require ["codemirror" :as CodeMirror]
  3. ["codemirror/addon/edit/closebrackets"]
  4. ["codemirror/addon/edit/matchbrackets"]
  5. ["codemirror/addon/hint/show-hint"]
  6. ["codemirror/addon/selection/active-line"]
  7. ["codemirror/mode/apl/apl"]
  8. ["codemirror/mode/asciiarmor/asciiarmor"]
  9. ["codemirror/mode/asn.1/asn.1"]
  10. ["codemirror/mode/asterisk/asterisk"]
  11. ["codemirror/mode/brainfuck/brainfuck"]
  12. ["codemirror/mode/clike/clike"]
  13. ["codemirror/mode/clojure/clojure"]
  14. ["codemirror/mode/cmake/cmake"]
  15. ["codemirror/mode/cobol/cobol"]
  16. ["codemirror/mode/coffeescript/coffeescript"]
  17. ["codemirror/mode/commonlisp/commonlisp"]
  18. ["codemirror/mode/crystal/crystal"]
  19. ["codemirror/mode/css/css"]
  20. ["codemirror/mode/cypher/cypher"]
  21. ["codemirror/mode/d/d"]
  22. ["codemirror/mode/dart/dart"]
  23. ["codemirror/mode/diff/diff"]
  24. ["codemirror/mode/django/django"]
  25. ["codemirror/mode/dockerfile/dockerfile"]
  26. ["codemirror/mode/dtd/dtd"]
  27. ["codemirror/mode/dylan/dylan"]
  28. ["codemirror/mode/ebnf/ebnf"]
  29. ["codemirror/mode/ecl/ecl"]
  30. ["codemirror/mode/eiffel/eiffel"]
  31. ["codemirror/mode/elm/elm"]
  32. ["codemirror/mode/erlang/erlang"]
  33. ["codemirror/mode/factor/factor"]
  34. ["codemirror/mode/fcl/fcl"]
  35. ["codemirror/mode/forth/forth"]
  36. ["codemirror/mode/fortran/fortran"]
  37. ["codemirror/mode/gas/gas"]
  38. ["codemirror/mode/gfm/gfm"]
  39. ["codemirror/mode/gherkin/gherkin"]
  40. ["codemirror/mode/go/go"]
  41. ["codemirror/mode/groovy/groovy"]
  42. ["codemirror/mode/haml/haml"]
  43. ["codemirror/mode/handlebars/handlebars"]
  44. ["codemirror/mode/haskell-literate/haskell-literate"]
  45. ["codemirror/mode/haskell/haskell"]
  46. ["codemirror/mode/haxe/haxe"]
  47. ["codemirror/mode/htmlembedded/htmlembedded"]
  48. ["codemirror/mode/htmlmixed/htmlmixed"]
  49. ["codemirror/mode/http/http"]
  50. ["codemirror/mode/idl/idl"]
  51. ["codemirror/mode/javascript/javascript"]
  52. ["codemirror/mode/jinja2/jinja2"]
  53. ["codemirror/mode/jsx/jsx"]
  54. ["codemirror/mode/julia/julia"]
  55. ["codemirror/mode/livescript/livescript"]
  56. ["codemirror/mode/lua/lua"]
  57. ["codemirror/mode/markdown/markdown"]
  58. ["codemirror/mode/mathematica/mathematica"]
  59. ["codemirror/mode/mbox/mbox"]
  60. ["codemirror/mode/meta"]
  61. ["codemirror/mode/mirc/mirc"]
  62. ["codemirror/mode/mllike/mllike"]
  63. ["codemirror/mode/modelica/modelica"]
  64. ["codemirror/mode/mscgen/mscgen"]
  65. ["codemirror/mode/mumps/mumps"]
  66. ["codemirror/mode/nginx/nginx"]
  67. ["codemirror/mode/nsis/nsis"]
  68. ["codemirror/mode/ntriples/ntriples"]
  69. ["codemirror/mode/octave/octave"]
  70. ["codemirror/mode/oz/oz"]
  71. ["codemirror/mode/pascal/pascal"]
  72. ["codemirror/mode/pegjs/pegjs"]
  73. ["codemirror/mode/perl/perl"]
  74. ["codemirror/mode/php/php"]
  75. ["codemirror/mode/pig/pig"]
  76. ["codemirror/mode/powershell/powershell"]
  77. ["codemirror/mode/properties/properties"]
  78. ["codemirror/mode/protobuf/protobuf"]
  79. ["codemirror/mode/pug/pug"]
  80. ["codemirror/mode/puppet/puppet"]
  81. ["codemirror/mode/python/python"]
  82. ["codemirror/mode/q/q"]
  83. ["codemirror/mode/r/r"]
  84. ["codemirror/mode/rpm/rpm"]
  85. ["codemirror/mode/rst/rst"]
  86. ["codemirror/mode/ruby/ruby"]
  87. ["codemirror/mode/rust/rust"]
  88. ["codemirror/mode/sas/sas"]
  89. ["codemirror/mode/sass/sass"]
  90. ["codemirror/mode/scheme/scheme"]
  91. ["codemirror/mode/shell/shell"]
  92. ["codemirror/mode/sieve/sieve"]
  93. ["codemirror/mode/slim/slim"]
  94. ["codemirror/mode/smalltalk/smalltalk"]
  95. ["codemirror/mode/smarty/smarty"]
  96. ["codemirror/mode/solr/solr"]
  97. ["codemirror/mode/soy/soy"]
  98. ["codemirror/mode/sparql/sparql"]
  99. ["codemirror/mode/spreadsheet/spreadsheet"]
  100. ["codemirror/mode/sql/sql"]
  101. ["codemirror/mode/stex/stex"]
  102. ["codemirror/mode/stylus/stylus"]
  103. ["codemirror/mode/swift/swift"]
  104. ["codemirror/mode/tcl/tcl"]
  105. ["codemirror/mode/textile/textile"]
  106. ["codemirror/mode/tiddlywiki/tiddlywiki"]
  107. ["codemirror/mode/tiki/tiki"]
  108. ["codemirror/mode/toml/toml"]
  109. ["codemirror/mode/tornado/tornado"]
  110. ["codemirror/mode/troff/troff"]
  111. ["codemirror/mode/ttcn-cfg/ttcn-cfg"]
  112. ["codemirror/mode/ttcn/ttcn"]
  113. ["codemirror/mode/turtle/turtle"]
  114. ["codemirror/mode/twig/twig"]
  115. ["codemirror/mode/vb/vb"]
  116. ["codemirror/mode/vbscript/vbscript"]
  117. ["codemirror/mode/velocity/velocity"]
  118. ["codemirror/mode/verilog/verilog"]
  119. ["codemirror/mode/vhdl/vhdl"]
  120. ["codemirror/mode/vue/vue"]
  121. ["codemirror/mode/wast/wast"]
  122. ["codemirror/mode/webidl/webidl"]
  123. ["codemirror/mode/xml/xml"]
  124. ["codemirror/mode/xquery/xquery"]
  125. ["codemirror/mode/yacas/yacas"]
  126. ["codemirror/mode/yaml-frontmatter/yaml-frontmatter"]
  127. ["codemirror/mode/yaml/yaml"]
  128. ["codemirror/mode/z80/z80"]
  129. [cljs-bean.core :as bean]
  130. [clojure.string :as string]
  131. [frontend.commands :as commands]
  132. [frontend.config :as config]
  133. [frontend.db :as db]
  134. [frontend.extensions.calc :as calc]
  135. [frontend.handler.code :as code-handler]
  136. [frontend.handler.editor :as editor-handler]
  137. [frontend.schema.handler.common-config :refer [Config-edn]]
  138. [frontend.state :as state]
  139. [frontend.util :as util]
  140. [goog.dom :as gdom]
  141. [goog.object :as gobj]
  142. [malli.core :as m]
  143. [promesa.core :as p]
  144. [rum.core :as rum]))
  145. ;; codemirror
  146. (def from-textarea (gobj/get CodeMirror "fromTextArea"))
  147. (def Pos (gobj/get CodeMirror "Pos"))
  148. (def textarea-ref-name "textarea")
  149. (def codemirror-ref-name "codemirror-instance")
  150. ;; export CodeMirror to global scope
  151. (set! js/window -CodeMirror CodeMirror)
  152. (defn- all-tokens-by-cursor
  153. "All tokens from the beginning of the document to the cursor(inclusive)."
  154. [cm]
  155. (let [cur (.getCursor cm)
  156. line (.-line cur)
  157. pos (.-ch cur)]
  158. (concat (mapcat #(.getLineTokens cm %) (range line))
  159. (filter #(<= (.-end %) pos) (.getLineTokens cm line)))))
  160. (defn- tokens->doc-state
  161. "Parse tokens into document state of the last token."
  162. [tokens]
  163. (let [init-state {:current-config-path []
  164. :state-stack (list :ok)}]
  165. (loop [state init-state
  166. tokens tokens]
  167. (if (empty? tokens)
  168. state
  169. (let [token (first tokens)
  170. token-type (.-type token)
  171. token-string (.-string token)
  172. current-state (first (:state-stack state))
  173. next-state (cond
  174. (or (nil? token-type)
  175. (= token-type "comment")
  176. (= token-type "meta") ;; TODO: handle meta prefix
  177. (= current-state :error))
  178. state
  179. (= token-type "bracket")
  180. (cond
  181. ;; ignore map if it is inside a list or vector (query or function)
  182. (and (= "{" token-string)
  183. (some #(contains? #{:list :vector} %)
  184. (:state-stack state)))
  185. (assoc state :state-stack (conj (:state-stack state) :ignore-map))
  186. (= "{" token-string)
  187. (assoc state :state-stack (conj (:state-stack state) :map))
  188. (= "(" token-string)
  189. (assoc state :state-stack (conj (:state-stack state) :list))
  190. (= "[" token-string)
  191. (assoc state :state-stack (conj (:state-stack state) :vector))
  192. (and (= :ignore-map current-state)
  193. (contains? #{"}" ")" "]"} token-string))
  194. (assoc state :state-stack (pop (:state-stack state)))
  195. (or (and (= "}" token-string) (= :map current-state))
  196. (and (= ")" token-string) (= :list current-state))
  197. (and (= "]" token-string) (= :vector current-state)))
  198. (let [new-state-stack (pop (:state-stack state))]
  199. (if (= (first new-state-stack) :key)
  200. (assoc state
  201. :state-stack (pop new-state-stack)
  202. :current-config-path (pop (:current-config-path state)))
  203. (assoc state :state-stack (pop (:state-stack state)))))
  204. :else
  205. (assoc state :state-stack (conj (:state-stack state) :error)))
  206. (and (= current-state :map) (= token-type "atom"))
  207. (assoc state
  208. :state-stack (conj (:state-stack state) :key)
  209. :current-config-path (conj (:current-config-path state) token-string))
  210. (= current-state :key)
  211. (assoc state
  212. :state-stack (pop (:state-stack state))
  213. :current-config-path (pop (:current-config-path state)))
  214. (or (= current-state :list) (= current-state :vector) (= current-state :ignore-map))
  215. state
  216. :else
  217. (assoc state :state-stack (conj (:state-stack state) :error)))]
  218. (recur next-state (rest tokens)))))))
  219. (defn- doc-state-at-cursor
  220. "Parse tokens into document state of last token."
  221. [cm]
  222. (let [tokens (all-tokens-by-cursor cm)
  223. {:keys [current-config-path state-stack]} (tokens->doc-state tokens)
  224. doc-state (first state-stack)]
  225. [current-config-path doc-state]))
  226. (defn- malli-type->completion-postfix
  227. [type]
  228. (case type
  229. :string "\"\""
  230. :map-of "{}"
  231. :map "{}"
  232. :set "#{}"
  233. :vector "[]"
  234. nil))
  235. ;; TODO: mu/to-map-syntax has been deprecated, consider removing usage
  236. (defn -map-syntax-walker [schema _ children _]
  237. (let [properties (m/properties schema)
  238. options (m/options schema)
  239. r (when properties (properties :registry))
  240. properties (if r (assoc properties :registry (m/-property-registry r options m/-form)) properties)]
  241. (cond-> {:type (m/type schema)}
  242. (seq properties) (assoc :properties properties)
  243. (seq children) (assoc :children children))))
  244. (defn- malli-to-map-syntax
  245. ([?schema] (malli-to-map-syntax ?schema nil))
  246. ([?schema options] (m/walk ?schema -map-syntax-walker options)))
  247. (.registerHelper CodeMirror "hint" "clojure"
  248. (fn [cm _options]
  249. (let [cur (.getCursor cm)
  250. token (.getTokenAt cm cur)
  251. token-type (.-type token)
  252. token-string (.-string token)
  253. result (atom {})
  254. [config-path doc-state] (doc-state-at-cursor cm)]
  255. (cond
  256. ;; completion of config keys, triggered by `:` or shortcut
  257. (and (= token-type "atom")
  258. (string/starts-with? token-string ":")
  259. (= doc-state :key))
  260. (do
  261. (m/walk Config-edn
  262. (fn [schema properties _children _opts]
  263. (let [schema-path (mapv str properties)]
  264. (cond
  265. (empty? schema-path)
  266. nil
  267. (empty? config-path)
  268. (swap! result assoc (first schema-path) (m/type schema))
  269. (= (count config-path) 1)
  270. (when (string/starts-with? (first schema-path) (first config-path))
  271. (swap! result assoc (first schema-path) (m/type schema)))
  272. (= (count config-path) 2)
  273. (when (and (= (count schema-path) 2)
  274. (= (first schema-path) (first config-path))
  275. (string/starts-with? (second schema-path) (second config-path)))
  276. (swap! result assoc (second schema-path) (m/type schema)))))
  277. nil))
  278. (when (not-empty @result)
  279. (let [from (Pos. (.-line cur) (.-start token))
  280. ;; `(.-ch cur)` is the cursor position, not the end of token. When completion is at the middle of a token, this is wrong
  281. to (Pos. (.-line cur) (.-end token))
  282. add-postfix-after? (<= (.-end token) (.-ch cur))
  283. doc (.getValue cm)
  284. list (->> (keys @result)
  285. (remove (fn [text]
  286. (re-find (re-pattern (str "[^;]*" text "\\s")) doc)))
  287. sort
  288. (map (fn [text]
  289. (let [type (get @result text)]
  290. {:text (str text (when add-postfix-after?
  291. (str " " (malli-type->completion-postfix type))))
  292. :displayText (str text " " type)}))))
  293. completion (clj->js {:list list
  294. :from from
  295. :to to})]
  296. completion)))
  297. ;; completion of :boolean, :enum, :keyword[TODO]
  298. (and (nil? token-type)
  299. (string/blank? (string/trim token-string))
  300. (not-empty config-path)
  301. (= doc-state :key))
  302. (do
  303. (m/walk Config-edn
  304. (fn [schema properties _children _opts]
  305. (let [schema-path (mapv str properties)]
  306. (when (= config-path schema-path)
  307. (case (m/type schema)
  308. :boolean
  309. (swap! result assoc
  310. "true" nil
  311. "false" nil)
  312. :enum
  313. (let [{:keys [children]} (malli-to-map-syntax schema)]
  314. (doseq [child children]
  315. (swap! result assoc (str child) nil)))
  316. nil))
  317. nil)))
  318. (when (not-empty @result)
  319. (let [from (Pos. (.-line cur) (.-ch cur))
  320. to (Pos. (.-line cur) (.-ch cur))
  321. list (->> (keys @result)
  322. sort
  323. (map (fn [text]
  324. {:text text
  325. :displayText text})))
  326. completion (clj->js {:list list
  327. :from from
  328. :to to})]
  329. completion)))))))
  330. (defn- complete-after
  331. [cm pred]
  332. (when (or (not pred) (pred))
  333. (js/setTimeout
  334. (fn []
  335. (when (not (.-completionActive (.-state cm)))
  336. (.showHint cm #js {:completeSingle false})))
  337. 100))
  338. (.-Pass CodeMirror))
  339. (defn- extra-codemirror-options []
  340. (get (state/get-config)
  341. :editor/extra-codemirror-options {}))
  342. (defn- text->cm-mode
  343. ([text]
  344. (text->cm-mode text :name))
  345. ([text by]
  346. (when text
  347. (let [mode (string/lower-case text)
  348. find-fn-name (case by
  349. :name "findModeByName"
  350. :ext "findModeByExtension"
  351. :file-name "findModeByFileName"
  352. "findModeByName")
  353. find-fn (gobj/get CodeMirror find-fn-name)
  354. cm-mode (find-fn mode)]
  355. (if cm-mode
  356. (.-mime cm-mode)
  357. mode)))))
  358. (defn- save-editor!
  359. [config]
  360. (p/do!
  361. (code-handler/save-code-editor!)
  362. (when-let [block (or (:code-block config) (:block config))]
  363. (let [block (db/entity [:block/uuid (:block/uuid block)])]
  364. (state/set-state! :editor/raw-mode-block block)
  365. (editor-handler/edit-block! block :max {:save-code-editor? false})))))
  366. (defn ^:large-vars/cleanup-todo render!
  367. [state]
  368. (let [[config id attr _code theme user-options] (:rum/args state)
  369. edit-block (:block config)
  370. code-block (:code-block config)
  371. config-file? (= (:file-path config) "logseq/config.edn")
  372. _ (state/set-state! :editor/code-mode? false)
  373. original-mode (get attr :data-lang)
  374. *editor-ref (get attr :editor-ref)
  375. mode (if (:file? config)
  376. (text->cm-mode original-mode :ext) ;; ref: src/main/frontend/components/file.cljs
  377. (text->cm-mode original-mode :name))
  378. lisp-like? (contains? #{"scheme" "lisp" "clojure" "edn"} mode)
  379. config-edit? (and (:file? config) (string/ends-with? (:file-path config) "config.edn"))
  380. textarea (gdom/getElement id)
  381. radix-color (state/sub :ui/radix-color)
  382. default-cm-options {:theme (if radix-color
  383. (str "lsradix " theme)
  384. (str "solarized " theme))
  385. :autoCloseBrackets true
  386. :lineNumbers true
  387. :matchBrackets lisp-like?
  388. :styleActiveLine true}
  389. cm-options (merge default-cm-options
  390. (cond-> (extra-codemirror-options)
  391. config-file?
  392. (dissoc :readOnly))
  393. {:mode mode
  394. :tabIndex -1 ;; do not accept TAB-in, since TAB is bind globally
  395. :extraKeys (merge {"Esc" (fn [cm]
  396. ;; Avoid reentrancy
  397. (gobj/set cm "escPressed" true)
  398. (save-editor! config))}
  399. (when config-edit?
  400. {"':'" complete-after
  401. "Ctrl-Space" "autocomplete"}))}
  402. (when config/publishing?
  403. {:readOnly true
  404. :cursorBlinkRate -1})
  405. (when config-edit?
  406. {:hintOptions {}})
  407. user-options)
  408. editor (when textarea
  409. (from-textarea textarea (clj->js cm-options)))
  410. _ (when (and editor *editor-ref)
  411. (reset! *editor-ref editor))]
  412. (when editor
  413. (let [textarea-ref (rum/ref-node state textarea-ref-name)
  414. element (.getWrapperElement editor)
  415. *cursor-prev (volatile! nil)
  416. *cursor-curr (volatile! nil)
  417. update-cursor-state! (fn []
  418. (let [start-pos (.getCursor editor true)
  419. end-pos (.getCursor editor false)
  420. start-pos' (bean/->clj (js/JSON.parse (js/JSON.stringify start-pos)))
  421. end-pos' (bean/->clj (js/JSON.parse (js/JSON.stringify end-pos)))
  422. range {:start (select-keys start-pos' [:line :ch])
  423. :end (select-keys end-pos' [:line :ch])}]
  424. (if (not @*cursor-prev)
  425. (vreset! *cursor-prev range)
  426. (vreset! *cursor-prev @*cursor-curr))
  427. (vreset! *cursor-curr range)))]
  428. (gobj/set textarea-ref codemirror-ref-name editor)
  429. (when (= mode "calc")
  430. (.on editor "change" (fn [_cm _e]
  431. (let [new-code (.getValue editor)]
  432. (reset! (:calc-atom state) (calc/eval-lines new-code))))))
  433. (.on editor "blur" (fn [cm e]
  434. (when e (util/stop e))
  435. (let [esc? (gobj/get cm "escPressed")]
  436. (when (or (= :file (state/get-current-route))
  437. (not esc?))
  438. (code-handler/save-code-editor!))
  439. (state/set-block-component-editing-mode! false)
  440. (state/set-state! :editor/code-block-context nil)
  441. (when (and (not esc?)
  442. (= (:db/id (state/get-edit-block))
  443. (:db/id edit-block)))
  444. (state/clear-edit!))
  445. (vreset! *cursor-curr nil)
  446. (vreset! *cursor-prev nil))))
  447. (.on editor "focus" (fn [_e]
  448. (when (and
  449. (contains? #{:code} (:logseq.property.node/display-type code-block))
  450. (not= (:block/uuid edit-block) (:block/uuid (state/get-edit-block))))
  451. (editor-handler/edit-block! (or code-block edit-block) :max {:container-id (:container-id config)}))
  452. (state/set-editing-block-dom-id! (:block-parent-id config))
  453. (state/set-block-component-editing-mode! true)
  454. (state/set-state! :editor/code-block-context
  455. {:editor editor
  456. :config config
  457. :state state})))
  458. (.on editor "cursorActivity" update-cursor-state!)
  459. (.addEventListener element "keydown" (fn [e]
  460. (let [key-code (.-code e)
  461. meta-or-ctrl-pressed? (or (.-ctrlKey e) (.-metaKey e))
  462. shifted? (.-shiftKey e)]
  463. (cond
  464. (contains? #{"ArrowLeft" "ArrowRight"} key-code)
  465. (let [direction (if (= "ArrowLeft" key-code) :left :right)]
  466. (when (and (= @*cursor-prev @*cursor-curr)
  467. (or (and direction (nil? @*cursor-curr))
  468. (case direction
  469. :left (and (zero? (:line (:start @*cursor-curr)))
  470. (zero? (:ch (:start @*cursor-curr))))
  471. :right (let [line (when-let [line (:line (:end @*cursor-curr))]
  472. (.getLine (.-doc editor) line))]
  473. (and (= (:line (:end @*cursor-curr)) (.lastLine editor))
  474. (= (:ch (:end @*cursor-curr)) (count line))))
  475. false)))
  476. (editor-handler/move-to-block-when-cross-boundary direction {}))
  477. (update-cursor-state!))
  478. (contains? #{"ArrowUp" "ArrowDown"} key-code)
  479. (let [direction (if (= "ArrowUp" key-code) :up :down)]
  480. (when (and (= @*cursor-prev @*cursor-curr)
  481. (or (and direction (nil? @*cursor-curr))
  482. (case direction
  483. :up (and (zero? (:line (:start @*cursor-curr)))
  484. (zero? (:ch (:start @*cursor-curr))))
  485. :down (let [line (when-let [line (:line (:end @*cursor-curr))]
  486. (.getLine (.-doc editor) line))]
  487. (and (= (:line (:end @*cursor-curr)) (.lastLine editor))
  488. (= (:ch (:end @*cursor-curr)) (count line))))
  489. false)))
  490. (editor-handler/move-cross-boundary-up-down
  491. direction {:input textarea
  492. :pos [direction 0]}))
  493. (update-cursor-state!))
  494. meta-or-ctrl-pressed?
  495. ;; prevent default behavior of browser
  496. ;; Cmd + [ => Go back in browser, outdent in CodeMirror
  497. ;; Cmd + ] => Go forward in browser, indent in CodeMirror
  498. (case key-code
  499. "BracketLeft" (util/stop e)
  500. "BracketRight" (util/stop e)
  501. nil)
  502. shifted?
  503. (case key-code
  504. ;; create new block
  505. "Enter"
  506. (do
  507. (util/stop e)
  508. (when-let [blockid (some-> (.-target e) (.closest "[blockid]") (.getAttribute "blockid"))]
  509. (code-handler/save-code-editor!)
  510. (util/schedule #(editor-handler/api-insert-new-block! ""
  511. {:block-uuid (uuid blockid)
  512. :sibling? true}))))
  513. nil)))))
  514. (.addEventListener element "pointerdown"
  515. (fn [e]
  516. (.stopPropagation e)
  517. (state/clear-selection!)))
  518. (.addEventListener element "touchstart"
  519. (fn [e]
  520. (.stopPropagation e)))
  521. (.save editor)
  522. (.refresh editor)))
  523. editor))
  524. (defn- load-and-render!
  525. [state]
  526. (let [editor-atom (:editor-atom state)]
  527. (when-not @editor-atom
  528. (let [editor (render! state)]
  529. (reset! editor-atom editor)))))
  530. (defn get-theme! []
  531. (if (state/sub :ui/radix-color)
  532. (str "lsradix " (state/sub :ui/theme))
  533. (str "solarized " (state/sub :ui/theme))))
  534. (rum/defcs editor < rum/reactive
  535. {:init (fn [state]
  536. (let [[_ _ _ code _ options] (:rum/args state)]
  537. (assoc state
  538. :editor-atom (atom nil)
  539. :calc-atom (atom (calc/eval-lines code))
  540. :code-options (atom options)
  541. :last-theme (atom (get-theme!)))))
  542. :did-mount (fn [state]
  543. (load-and-render! state)
  544. state)
  545. :did-update (fn [state]
  546. (let [next-theme (get-theme!)
  547. last-theme @(:last-theme state)
  548. editor' (some-> state :editor-atom deref)]
  549. (when (and editor' (not= next-theme last-theme))
  550. (reset! (:last-theme state) next-theme)
  551. (.setOption editor' "theme" next-theme)))
  552. (reset! (:code-options state) (last (:rum/args state)))
  553. (when-not (:file? (first (:rum/args state)))
  554. (let [code (nth (:rum/args state) 3)
  555. editor' @(:editor-atom state)]
  556. (when (and editor' (not= (.getValue editor') code))
  557. (.setValue editor' code))))
  558. state)}
  559. [state _config id attr code _theme _options]
  560. [:div.extensions__code.flex.flex-1
  561. (when-let [mode (:data-lang attr)]
  562. (when-not (= mode "calc")
  563. [:div.extensions__code-lang
  564. (string/lower-case mode)]))
  565. [:div.code-editor.flex.flex-1.flex-row.w-full
  566. [:textarea (merge {:id id
  567. ;; Expose the textarea associated with the CodeMirror instance via
  568. ;; ref so that we can autofocus into the CodeMirror instance later.
  569. :ref textarea-ref-name
  570. :default-value code} attr)]
  571. (when (= (:data-lang attr) "calc")
  572. (calc/results (:calc-atom state)))]])
  573. ;; Focus into the CodeMirror editor rather than the normal "raw" editor
  574. (defmethod commands/handle-step :codemirror/focus [[_]]
  575. ;; This requestAnimationFrame is necessary because, for some reason, when you
  576. ;; type /calculate and then click the "Calculate" command in the dropdown
  577. ;; *with your mouse* (but not when you do so via your keyboard with the
  578. ;; arrow + enter keys!), React doesn't re-render before the :codemirror/focus
  579. ;; command kicks off. As a result, you get an error saying that the node
  580. ;; you're trying to focus doesn't yet exist. Adding the requestAnimationFrame
  581. ;; ensures that the React component re-renders before the :codemirror/focus
  582. ;; command is run. It's not elegant... open to suggestions for how to fix it!
  583. (let [block (state/get-edit-block)
  584. block-uuid (:block/uuid block)]
  585. (p/do!
  586. (state/pub-event! [:editor/save-current-block])
  587. (state/clear-edit!)
  588. (js/setTimeout
  589. (fn []
  590. (let [block-node (util/get-first-block-by-id block-uuid)
  591. textarea-ref (.querySelector block-node "textarea")]
  592. (when-let [codemirror-ref (gobj/get textarea-ref codemirror-ref-name)]
  593. (.focus codemirror-ref))))
  594. 256))))