code.cljs 10 KB

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