code.cljs 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226
  1. (ns frontend.extensions.code
  2. (:require [rum.core :as rum]
  3. [frontend.util :as util]
  4. [goog.dom :as gdom]
  5. [goog.object :as gobj]
  6. [frontend.db :as db]
  7. [frontend.extensions.calc :as calc]
  8. [frontend.commands :as commands]
  9. [frontend.state :as state]
  10. [frontend.handler.editor :as editor-handler]
  11. [frontend.handler.file :as file-handler]
  12. [clojure.string :as string]
  13. [dommy.core :as dom]
  14. [frontend.utf8 :as utf8]
  15. ["codemirror" :as cm]
  16. ["codemirror/addon/edit/matchbrackets"]
  17. ["codemirror/addon/edit/closebrackets"]
  18. ["codemirror/addon/selection/active-line"]
  19. ["codemirror/mode/clojure/clojure"]
  20. ["codemirror/mode/powershell/powershell"]
  21. ["codemirror/mode/javascript/javascript"]
  22. ["codemirror/mode/jsx/jsx"]
  23. ["codemirror/mode/clike/clike"]
  24. ["codemirror/mode/vue/vue"]
  25. ["codemirror/mode/commonlisp/commonlisp"]
  26. ["codemirror/mode/coffeescript/coffeescript"]
  27. ["codemirror/mode/css/css"]
  28. ["codemirror/mode/sass/sass"]
  29. ["codemirror/mode/dart/dart"]
  30. ["codemirror/mode/dockerfile/dockerfile"]
  31. ["codemirror/mode/elm/elm"]
  32. ["codemirror/mode/erlang/erlang"]
  33. ["codemirror/mode/go/go"]
  34. ["codemirror/mode/haskell/haskell"]
  35. ["codemirror/mode/lua/lua"]
  36. ["codemirror/mode/mathematica/mathematica"]
  37. ["codemirror/mode/perl/perl"]
  38. ["codemirror/mode/php/php"]
  39. ["codemirror/mode/python/python"]
  40. ["codemirror/mode/ruby/ruby"]
  41. ["codemirror/mode/rust/rust"]
  42. ["codemirror/mode/scheme/scheme"]
  43. ["codemirror/mode/shell/shell"]
  44. ["codemirror/mode/smalltalk/smalltalk"]
  45. ["codemirror/mode/sql/sql"]
  46. ["codemirror/mode/swift/swift"]
  47. ["codemirror/mode/xml/xml"]
  48. ["codemirror/mode/sparql/sparql"]
  49. ["codemirror/mode/turtle/turtle"]
  50. ["codemirror/mode/r/r"]
  51. ["codemirror/mode/julia/julia"]))
  52. ;; codemirror
  53. (def from-textarea (gobj/get cm "fromTextArea"))
  54. (def textarea-ref-name "textarea")
  55. (def codemirror-ref-name "codemirror-instance")
  56. (defn- save-file-or-block-when-blur-or-esc!
  57. [editor textarea config state]
  58. (.save editor)
  59. (let [value (gobj/get textarea "value")
  60. default-value (gobj/get textarea "defaultValue")]
  61. (when (not= value default-value)
  62. (cond
  63. (:block/uuid config)
  64. (let [block (db/pull [:block/uuid (:block/uuid config)])
  65. format (:block/format block)
  66. content (:block/content block)
  67. full-content (:full_content (last (:rum/args state)))]
  68. (when (and full-content (string/includes? content full-content))
  69. (let [lines (string/split-lines full-content)
  70. fl (first lines)
  71. ll (last lines)]
  72. (when (and fl ll)
  73. (let [value' (str (string/trim fl) "\n" value "\n" (string/trim ll))
  74. ;; FIXME: What if there're multiple code blocks with the same value?
  75. content' (string/replace-first content full-content value')]
  76. (editor-handler/save-block-if-changed! block content'))))))
  77. (:file-path config)
  78. (let [path (:file-path config)
  79. content (db/get-file-no-sub path)
  80. value (some-> (gdom/getElement path)
  81. (gobj/get "value"))]
  82. (when (and
  83. (not (string/blank? value))
  84. (not= (string/trim value) (string/trim content)))
  85. (file-handler/alter-file (state/get-current-repo) path (string/trim value)
  86. {:re-render-root? true})))
  87. :else
  88. nil))))
  89. (defn- text->cm-mode
  90. [text]
  91. (when text
  92. (let [mode (string/lower-case text)]
  93. (case mode
  94. "html" "text/html"
  95. "c" "text/x-csrc"
  96. "c++" "text/x-c++src"
  97. "java" "text/x-java"
  98. "c#" "text/x-csharp"
  99. "csharp" "text/x-csharp"
  100. "objective-c" "text/x-objectivec"
  101. "scala" "text/x-scala"
  102. "js" "text/javascript"
  103. "typescript" "text/typescript"
  104. "ts" "text/typescript"
  105. "tsx" "text/typescript-jsx"
  106. "scss" "text/x-scss"
  107. "less" "text/x-less"
  108. mode))))
  109. (defn render!
  110. [state]
  111. (let [esc-pressed? (atom nil)
  112. dark? (state/dark?)]
  113. (let [[config id attr code theme] (:rum/args state)
  114. original-mode (get attr :data-lang)
  115. mode original-mode
  116. clojure? (contains? #{"clojure" "clj" "text/x-clojure" "cljs" "cljc"} mode)
  117. mode (if clojure? "clojure" (text->cm-mode mode))
  118. lisp? (or clojure?
  119. (contains? #{"scheme" "racket" "lisp"} mode))
  120. textarea (gdom/getElement id)
  121. editor (when textarea
  122. (from-textarea textarea
  123. #js {:mode mode
  124. :theme (str "solarized " theme)
  125. :matchBrackets lisp?
  126. :autoCloseBrackets true
  127. :lineNumbers true
  128. :styleActiveLine true
  129. :extraKeys #js {"Esc" (fn [cm]
  130. (save-file-or-block-when-blur-or-esc! cm textarea config state)
  131. (when-let [block-id (:block/uuid config)]
  132. (let [block (db/pull [:block/uuid block-id])
  133. value (.getValue cm)
  134. textarea-value (gobj/get textarea "value")]
  135. (editor-handler/edit-block! block :max (:block/format block) block-id)))
  136. ;; TODO: return "handled" or false doesn't always prevent event bubbles
  137. (reset! esc-pressed? true)
  138. (js/setTimeout #(reset! esc-pressed? false) 10))}}))]
  139. (when editor
  140. (let [textarea-ref (rum/ref-node state textarea-ref-name)]
  141. (gobj/set textarea-ref codemirror-ref-name editor))
  142. (let [element (.getWrapperElement editor)]
  143. (when (= mode "calc")
  144. (.on editor "change" (fn [_cm e]
  145. (let [new-code (.getValue editor)]
  146. (reset! (:calc-atom state) (calc/eval-lines new-code))))))
  147. (.on editor "blur" (fn [_cm e]
  148. (when e (util/stop e))
  149. (state/set-block-component-editing-mode! false)
  150. (when-not @esc-pressed?
  151. (save-file-or-block-when-blur-or-esc! editor textarea config state))))
  152. (.addEventListener element "mousedown"
  153. (fn [e]
  154. (state/clear-selection!)
  155. (when-let [block (and (:block/uuid config) (into {} (db/get-block-by-uuid (:block/uuid config))))]
  156. (state/set-editing! id (.getValue editor) block nil false))
  157. (util/stop e)
  158. (state/set-block-component-editing-mode! true)))
  159. (.save editor)
  160. (.refresh editor)))
  161. editor)))
  162. (defn- load-and-render!
  163. [state]
  164. (let [editor-atom (:editor-atom state)
  165. editor (render! state)]
  166. (reset! editor-atom editor)))
  167. (rum/defcs editor < rum/reactive
  168. {:init (fn [state]
  169. (let [[_ _ _ code _] (:rum/args state)]
  170. (assoc state :editor-atom (atom nil) :calc-atom (atom (calc/eval-lines code)))))
  171. :did-mount (fn [state]
  172. (load-and-render! state)
  173. state)
  174. :did-update (fn [state]
  175. (when-let [editor @(:editor-atom state)]
  176. ;; clear the previous instance
  177. (.toTextArea ^js editor))
  178. (load-and-render! state)
  179. state)}
  180. [state config id attr code theme options]
  181. [:div.extensions__code
  182. (when-let [mode (:data-lang attr)]
  183. (when-not (= mode "calc")
  184. [:div.extensions__code-lang
  185. (let [mode (string/lower-case mode)]
  186. (if (= mode "text/x-clojure")
  187. "clojure"
  188. mode))]))
  189. [:textarea (merge {:id id
  190. ;; Expose the textarea associated with the CodeMirror instance via
  191. ;; ref so that we can autofocus into the CodeMirror instance later.
  192. :ref textarea-ref-name
  193. :default-value code} attr)]
  194. (when (= (:data-lang attr) "calc")
  195. (calc/results (:calc-atom state)))])
  196. ;; Focus into the CodeMirror editor rather than the normal "raw" editor
  197. (defmethod commands/handle-step :codemirror/focus [[_]]
  198. ;; This requestAnimationFrame is necessary because, for some reason, when you
  199. ;; type /calculate and then click the "Calculate" command in the dropdown
  200. ;; *with your mouse* (but not when you do so via your keyboard with the
  201. ;; arrow + enter keys!), React doesn't re-render before the :codemirror/focus
  202. ;; command kicks off. As a result, you get an error saying that the node
  203. ;; you're trying to focus doesn't yet exist. Adding the requestAnimationFrame
  204. ;; ensures that the React component re-renders before the :codemirror/focus
  205. ;; command is run. It's not elegant... open to suggestions for how to fix it!
  206. (js/window.requestAnimationFrame
  207. (fn []
  208. (let [block (state/get-edit-block)
  209. block-uuid (:block/uuid block)
  210. block-node (util/get-first-block-by-id block-uuid)]
  211. (editor-handler/select-block! (:block/uuid block))
  212. (let [textarea-ref (.querySelector block-node "textarea")]
  213. (.focus (gobj/get textarea-ref codemirror-ref-name)))
  214. (util/select-unhighlight! (dom/by-class "selected"))
  215. (state/clear-selection!)))))