commands.cljs 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566
  1. (ns frontend.commands
  2. (:require [frontend.util :as util]
  3. [frontend.util.cursor :as cursor]
  4. [frontend.util.marker :as marker]
  5. [frontend.util.priority :as priority]
  6. [frontend.date :as date]
  7. [frontend.state :as state]
  8. [frontend.search :as search]
  9. [frontend.config :as config]
  10. [frontend.db.utils :as db-util]
  11. [frontend.db :as db]
  12. [clojure.string :as string]
  13. [goog.dom :as gdom]
  14. [goog.object :as gobj]
  15. [frontend.format :as format]
  16. [frontend.handler.common :as common-handler]
  17. [frontend.handler.plugin :as plugin-handler]
  18. [frontend.handler.draw :as draw]
  19. [frontend.handler.notification :as notification]
  20. [promesa.core :as p]))
  21. ;; TODO: move to frontend.handler.editor.commands
  22. (defonce *show-commands (atom false))
  23. (defonce *slash-caret-pos (atom nil))
  24. (defonce slash "/")
  25. (defonce *show-block-commands (atom false))
  26. (defonce angle-bracket "<")
  27. (defonce *angle-bracket-caret-pos (atom nil))
  28. (defonce *current-command (atom nil))
  29. (def link-steps [[:editor/input (str slash "link")]
  30. [:editor/show-input [{:command :link
  31. :id :link
  32. :placeholder "Link"}
  33. {:command :link
  34. :id :label
  35. :placeholder "Label"}]]])
  36. (defn ->marker
  37. [marker]
  38. [[:editor/clear-current-slash]
  39. [:editor/set-marker marker]
  40. [:editor/move-cursor-to-end]])
  41. (defn ->priority
  42. [priority]
  43. [[:editor/clear-current-slash]
  44. [:editor/set-priority priority]
  45. [:editor/move-cursor-to-end]])
  46. (defn ->inline
  47. [type]
  48. (let [template (util/format "@@%s: @@"
  49. type)]
  50. [[:editor/input template {:last-pattern slash
  51. :backward-pos 2}]]))
  52. (defn embed-page
  53. []
  54. (conj
  55. [[:editor/input "{{embed [[]]}}" {:last-pattern slash
  56. :backward-pos 4}]]
  57. [:editor/search-page :embed]))
  58. (defn embed-block
  59. []
  60. [[:editor/input "{{embed (())}}" {:last-pattern slash
  61. :backward-pos 4}]
  62. [:editor/search-block :embed]])
  63. ;; Stop now!!
  64. ;; (def commands-plugins
  65. ;; {"Encrypt text" {:steps [[:editor/input (str slash "encrypt")]
  66. ;; [:editor/show-input [{:id :hint
  67. ;; :placeholder "Hint"}
  68. ;; {:id :password
  69. ;; :type "password"}]]]
  70. ;; :insert-fn (fn [hint password]
  71. ;; (util/format "{{{encrypt %s}}}"
  72. ;; (pr-str {:hint hint
  73. ;; :content content})))}})
  74. (defn get-preferred-workflow
  75. []
  76. (let [workflow (state/get-preferred-workflow)]
  77. (if (= :now workflow)
  78. [["LATER" (->marker "LATER")]
  79. ["NOW" (->marker "NOW")]
  80. ["TODO" (->marker "TODO")]
  81. ["DOING" (->marker "DOING")]]
  82. [["TODO" (->marker "TODO")]
  83. ["DOING" (->marker "DOING")]
  84. ["LATER" (->marker "LATER")]
  85. ["NOW" (->marker "NOW")]])))
  86. ;; Credits to roamresearch.com
  87. (defn- ->heading
  88. [heading]
  89. [[:editor/clear-current-slash]
  90. [:editor/set-heading heading]
  91. [:editor/move-cursor-to-end]])
  92. (defn- markdown-headings
  93. []
  94. (let [format (state/get-preferred-format)]
  95. (when (= (name format) "markdown")
  96. (mapv (fn [level]
  97. (let [heading (str "h" level)]
  98. [heading (->heading (apply str (repeat level "#")))])) (range 1 7)))))
  99. (defonce *matched-commands (atom nil))
  100. (defonce *initial-commands (atom nil))
  101. (defonce *first-command-group
  102. {"Page Reference" "BASIC"
  103. "Tomorrow" "TIME & DATE"
  104. "LATER" "TASK"
  105. "A" "PRIORITY"
  106. "Query" "ADVANCED"
  107. "Quote" "ORG-MODE"})
  108. (defn ->block
  109. ([type]
  110. (->block type nil))
  111. ([type optional]
  112. (let [format (get (state/get-edit-block) :block/format :markdown)
  113. org? (= format :org)
  114. t (string/lower-case type)
  115. markdown-src? (and (= format :markdown) (= t "src"))
  116. left (cond
  117. markdown-src?
  118. "```"
  119. :else
  120. (util/format "#+BEGIN_%s"
  121. (string/upper-case type)))
  122. right (if markdown-src?
  123. (str "\n```")
  124. (util/format "\n#+END_%s" (string/upper-case type)))
  125. template (str
  126. left
  127. (if optional (str " " optional) "")
  128. "\n"
  129. right)
  130. backward-pos (if (= type "src")
  131. (+ 1 (count right))
  132. (count right))]
  133. [[:editor/input template {:last-pattern angle-bracket
  134. :backward-pos backward-pos}]])))
  135. (defn ->properties
  136. []
  137. (let [template (util/format
  138. ":PROPERTIES:\n:: \n:END:\n")
  139. backward-pos 9]
  140. [[:editor/input template {:last-pattern angle-bracket
  141. :backward-pos backward-pos}]]))
  142. ;; https://orgmode.org/manual/Structure-Templates.html
  143. (defn block-commands-map
  144. []
  145. (->>
  146. (concat
  147. [["Quote" (->block "quote")]
  148. ["Src" (->block "src" "")]
  149. ["Query" (->block "query")]
  150. ["Latex export" (->block "export" "latex")]
  151. ;; FIXME: current page's format
  152. (when (= :org (state/get-preferred-format))
  153. ["Properties" (->properties)])
  154. ["Note" (->block "note")]
  155. ["Tip" (->block "tip")]
  156. ["Important" (->block "important")]
  157. ["Caution" (->block "caution")]
  158. ["Pinned" (->block "pinned")]
  159. ["Warning" (->block "warning")]
  160. ["Example" (->block "example")]
  161. ["Export" (->block "export")]
  162. ["Verse" (->block "verse")]
  163. ["Ascii" (->block "export" "ascii")]
  164. ["Center" (->block "export")]
  165. ["Comment" (->block "comment")]]
  166. ;; Allow user to modify or extend, should specify how to extend.
  167. (state/get-commands))
  168. (remove nil?)
  169. (util/distinct-by-last-wins first)))
  170. (defn commands-map
  171. [get-page-ref-text]
  172. (->>
  173. (concat
  174. ;; basic
  175. [["Page Reference" [[:editor/input "[[]]" {:backward-pos 2}]
  176. [:editor/search-page]] "Create a backlink to a page"]
  177. ["Page Embed" (embed-page) "Embed a page here"]
  178. ["Block Reference" [[:editor/input "(())" {:backward-pos 2}]
  179. [:editor/search-block :reference]] "Create a backlink to a blcok"]
  180. ["Block Embed" (embed-block) "Embed a block here" "Embed a block here"]
  181. ["Link" link-steps "Create a HTTP link"]
  182. ["Image Link" link-steps "Create a HTTP link to a image"]
  183. (when (state/markdown?)
  184. ["Underline" [[:editor/input "<ins></ins>"
  185. {:last-pattern slash
  186. :backward-pos 6}]] "Create a underline text decoration"])
  187. ["Template" [[:editor/input "/" nil]
  188. [:editor/search-template]] "Insert a created template here"]
  189. (cond
  190. (and (util/electron?) (config/local-db? (state/get-current-repo)))
  191. ["Upload an asset" [[:editor/click-hidden-file-input :id]] "Upload file types like image, pdf, docx, etc.)"]
  192. (state/logged?)
  193. ["Upload an image" [[:editor/click-hidden-file-input :id]]])]
  194. (markdown-headings)
  195. ;; time & date
  196. [["Tomorrow" #(get-page-ref-text (date/tomorrow)) "Insert the date of tomorrow"]
  197. ["Yesterday" #(get-page-ref-text (date/yesterday)) "Insert the date of yesterday"]
  198. ["Today" #(get-page-ref-text (date/today)) "Insert the date of today"]
  199. ["Current Time" #(date/get-current-time) "Insert current time"]
  200. ["Date Picker" [[:editor/show-date-picker]] "Pick a date and insert here"]]
  201. ;; task management
  202. (get-preferred-workflow)
  203. [["DONE" (->marker "DONE")]
  204. ["WAITING" (->marker "WAITING")]
  205. ["CANCELED" (->marker "CANCELED")]
  206. ["Deadline" [[:editor/clear-current-slash]
  207. [:editor/show-date-picker :deadline]]]
  208. ["Scheduled" [[:editor/clear-current-slash]
  209. [:editor/show-date-picker :scheduled]]]]
  210. ;; priority
  211. [["A" (->priority "A")]
  212. ["B" (->priority "B")]
  213. ["C" (->priority "C")]]
  214. ;; advanced
  215. [["Query" [[:editor/input "{{query }}" {:backward-pos 2}]] "Create a DataScript query"]
  216. ["Query table function" [[:editor/input "{{function }}" {:backward-pos 2}]] "Create a query table function"]
  217. ["Calculator" [[:editor/input "```calc\n\n```" {:backward-pos 4}]
  218. [:codemirror/focus]] "Insert a calculator"]
  219. ["Draw" (fn []
  220. (let [file (draw/file-name)
  221. path (str config/default-draw-directory "/" file)
  222. text (util/format "[[%s]]" path)]
  223. (p/let [_ (draw/create-draw-with-default-content path)]
  224. (println "draw file created, " path))
  225. text)) "Draw a graph with Excalidraw"]
  226. (when (util/zh-CN-supported?)
  227. ["Embed Bilibili Video" [[:editor/input "{{bilibili }}" {:last-pattern slash
  228. :backward-pos 2}]]])
  229. ["Embed HTML " (->inline "html")]
  230. ["Embed Youtube Video" [[:editor/input "{{youtube }}" {:last-pattern slash
  231. :backward-pos 2}]]]
  232. ["Embed Vimeo Video" [[:editor/input "{{vimeo }}" {:last-pattern slash
  233. :backward-pos 2}]]]]
  234. ;; Allow user to modify or extend, should specify how to extend.
  235. (state/get-commands)
  236. (state/get-plugins-commands))
  237. (remove nil?)
  238. (util/distinct-by-last-wins first)))
  239. (defn init-commands!
  240. [get-page-ref-text]
  241. (let [commands (commands-map get-page-ref-text)]
  242. (reset! *initial-commands commands)
  243. (reset! *matched-commands commands)))
  244. (defonce *matched-block-commands (atom (block-commands-map)))
  245. (defn restore-state
  246. [restore-slash-caret-pos?]
  247. (when restore-slash-caret-pos?
  248. (reset! *slash-caret-pos nil))
  249. (reset! *show-commands false)
  250. (reset! *matched-commands @*initial-commands)
  251. (reset! *angle-bracket-caret-pos nil)
  252. (reset! *show-block-commands false)
  253. (reset! *matched-block-commands (block-commands-map)))
  254. (defn insert!
  255. [id value
  256. {:keys [last-pattern postfix-fn backward-pos forward-pos]
  257. :or {last-pattern slash}
  258. :as option}]
  259. (when-let [input (gdom/getElement id)]
  260. (let [edit-content (gobj/get input "value")
  261. current-pos (cursor/pos input)
  262. prefix (subs edit-content 0 current-pos)
  263. space? (when (and last-pattern prefix)
  264. (let [s (when-let [last-index (string/last-index-of prefix last-pattern)]
  265. (util/safe-subs prefix 0 last-index))]
  266. (not (and s
  267. (string/ends-with? s "(")
  268. (or (string/starts-with? last-pattern "((")
  269. (string/starts-with? last-pattern "[["))))))
  270. space? (if (and space? (string/starts-with? last-pattern "#[["))
  271. false
  272. space?)
  273. prefix (if (string/blank? last-pattern)
  274. (if space?
  275. (util/concat-without-spaces prefix value)
  276. (str prefix value))
  277. (util/replace-last last-pattern prefix value space?))
  278. postfix (subs edit-content current-pos)
  279. postfix (if postfix-fn (postfix-fn postfix) postfix)
  280. new-value (if space?
  281. (util/concat-without-spaces prefix postfix)
  282. (str prefix postfix))
  283. new-pos (- (+ (count prefix)
  284. (or forward-pos 0))
  285. (or backward-pos 0))]
  286. (state/set-block-content-and-last-pos! id new-value new-pos)
  287. (cursor/move-cursor-to input
  288. (if (or backward-pos forward-pos)
  289. new-pos
  290. (+ new-pos 1))))))
  291. (defn simple-insert!
  292. [id value
  293. {:keys [backward-pos forward-pos check-fn]
  294. :as option}]
  295. (let [input (gdom/getElement id)
  296. edit-content (gobj/get input "value")
  297. current-pos (cursor/pos input)
  298. prefix (subs edit-content 0 current-pos)
  299. new-value (str prefix
  300. value
  301. (subs edit-content current-pos))
  302. new-pos (- (+ (count prefix)
  303. (count value)
  304. (or forward-pos 0))
  305. (or backward-pos 0))]
  306. (state/set-block-content-and-last-pos! id new-value new-pos)
  307. (cursor/move-cursor-to input new-pos)
  308. (when check-fn
  309. (check-fn new-value (dec (count prefix)) new-pos))))
  310. (defn insert-before!
  311. [id value
  312. {:keys [backward-pos forward-pos check-fn]
  313. :as option}]
  314. (let [input (gdom/getElement id)
  315. edit-content (gobj/get input "value")
  316. current-pos (cursor/pos input)
  317. suffix (subs edit-content 0 current-pos)
  318. new-value (str value
  319. suffix
  320. (subs edit-content current-pos))
  321. new-pos (- (+ (count suffix)
  322. (count value)
  323. (or forward-pos 0))
  324. (or backward-pos 0))]
  325. (state/set-block-content-and-last-pos! id new-value new-pos)
  326. (cursor/move-cursor-to input new-pos)
  327. (when check-fn
  328. (check-fn new-value (dec (count suffix)) new-pos))))
  329. (defn simple-replace!
  330. [id value selected
  331. {:keys [backward-pos forward-pos check-fn]
  332. :as option}]
  333. (let [selected? (not (string/blank? selected))
  334. input (gdom/getElement id)
  335. edit-content (gobj/get input "value")
  336. current-pos (cursor/pos input)
  337. prefix (subs edit-content 0 current-pos)
  338. postfix (if selected?
  339. (string/replace-first (subs edit-content current-pos)
  340. selected
  341. "")
  342. (subs edit-content current-pos))
  343. new-value (str prefix value postfix)
  344. new-pos (- (+ (count prefix)
  345. (count value)
  346. (or forward-pos 0))
  347. (or backward-pos 0))]
  348. (state/set-block-content-and-last-pos! id new-value new-pos)
  349. (cursor/move-cursor-to input new-pos)
  350. (when selected?
  351. (.setSelectionRange input new-pos (+ new-pos (count selected))))
  352. (when check-fn
  353. (check-fn new-value (dec (count prefix))))))
  354. (defn delete-pair!
  355. [id]
  356. (let [input (gdom/getElement id)
  357. edit-content (gobj/get input "value")
  358. current-pos (cursor/pos input)
  359. prefix (subs edit-content 0 (dec current-pos))
  360. new-value (str prefix
  361. (subs edit-content (inc current-pos)))
  362. new-pos (count prefix)]
  363. (state/set-block-content-and-last-pos! id new-value new-pos)
  364. (cursor/move-cursor-to input new-pos)))
  365. (defn get-matched-commands
  366. ([text]
  367. (get-matched-commands text @*initial-commands))
  368. ([text commands]
  369. (search/fuzzy-search commands text
  370. :extract-fn first
  371. :limit 50)))
  372. (defn get-command-input
  373. [edit-content]
  374. (when-not (string/blank? edit-content)
  375. (let [result (last (util/split-last slash edit-content))]
  376. (if (string/blank? result)
  377. nil
  378. result))))
  379. (defmulti handle-step first)
  380. (defmethod handle-step :editor/hook [[_ event {:keys [pid uuid] :as payload}] format]
  381. (plugin-handler/hook-plugin-editor event (merge payload {:format format :uuid (or uuid (:block/uuid (state/get-edit-block)))}) pid))
  382. (defmethod handle-step :editor/input [[_ value option]]
  383. (when-let [input-id (state/get-edit-input-id)]
  384. (insert! input-id value option)))
  385. (defmethod handle-step :editor/cursor-back [[_ n]]
  386. (when-let [input-id (state/get-edit-input-id)]
  387. (when-let [current-input (gdom/getElement input-id)]
  388. (cursor/move-cursor-backward current-input n))))
  389. (defmethod handle-step :editor/cursor-forward [[_ n]]
  390. (when-let [input-id (state/get-edit-input-id)]
  391. (when-let [current-input (gdom/getElement input-id)]
  392. (cursor/move-cursor-forward current-input n))))
  393. (defmethod handle-step :editor/move-cursor-to-end [[_]]
  394. (when-let [input-id (state/get-edit-input-id)]
  395. (when-let [current-input (gdom/getElement input-id)]
  396. (cursor/move-cursor-to-end current-input))))
  397. (defmethod handle-step :editor/restore-saved-cursor [[_]]
  398. (when-let [input-id (state/get-edit-input-id)]
  399. (when-let [current-input (gdom/getElement input-id)]
  400. (cursor/move-cursor-to current-input (:editor/last-saved-cursor @state/state)))))
  401. (defmethod handle-step :editor/clear-current-slash [[_ space?]]
  402. (when-let [input-id (state/get-edit-input-id)]
  403. (when-let [current-input (gdom/getElement input-id)]
  404. (let [edit-content (gobj/get current-input "value")
  405. current-pos (cursor/pos current-input)
  406. prefix (subs edit-content 0 current-pos)
  407. prefix (util/replace-last slash prefix "" (boolean space?))
  408. new-value (str prefix
  409. (subs edit-content current-pos))]
  410. (state/set-block-content-and-last-pos! input-id
  411. new-value
  412. (count prefix))))))
  413. (defn compute-pos-delta-when-change-marker
  414. [current-input edit-content new-value marker pos]
  415. (let [old-marker (some->> (first (util/safe-re-find marker/bare-marker-pattern edit-content))
  416. (string/trim))
  417. old-marker (if old-marker old-marker "")
  418. pos-delta (- (count marker)
  419. (count old-marker))
  420. pos-delta (if (string/blank? old-marker)
  421. (inc pos-delta)
  422. pos-delta)]
  423. (+ pos pos-delta)))
  424. (defmethod handle-step :editor/set-marker [[_ marker] format]
  425. (when-let [input-id (state/get-edit-input-id)]
  426. (when-let [current-input (gdom/getElement input-id)]
  427. (let [edit-content (gobj/get current-input "value")
  428. slash-pos (:pos @*slash-caret-pos)
  429. [re-pattern new-line-re-pattern] (if (= :org format)
  430. [#"\*+\s" #"\n\*+\s"]
  431. [#"#+\s" #"\n#+\s"])
  432. pos (let [prefix (subs edit-content 0 (dec slash-pos))]
  433. (if-let [matches (seq (util/re-pos new-line-re-pattern prefix))]
  434. (let [[start-pos content] (last matches)]
  435. (+ start-pos (count content)))
  436. (count (util/safe-re-find re-pattern prefix))))
  437. new-value (str (subs edit-content 0 pos)
  438. (string/replace-first (subs edit-content pos)
  439. marker/marker-pattern
  440. (str marker " ")))]
  441. (state/set-edit-content! input-id new-value)
  442. (let [new-pos (compute-pos-delta-when-change-marker
  443. current-input edit-content new-value marker (dec slash-pos))]
  444. ;; TODO: any performance issue?
  445. (js/setTimeout #(cursor/move-cursor-to current-input new-pos) 10))))))
  446. (defmethod handle-step :editor/set-priority [[_ priority] format]
  447. (when-let [input-id (state/get-edit-input-id)]
  448. (when-let [current-input (gdom/getElement input-id)]
  449. (let [format (or (db/get-page-format (state/get-current-page)) (state/get-preferred-format))
  450. edit-content (gobj/get current-input "value")
  451. new-priority (util/format "[#%s]" priority)
  452. new-value (string/trim (priority/add-or-update-priority edit-content format new-priority))]
  453. (state/set-edit-content! input-id new-value)))))
  454. (defmethod handle-step :editor/set-heading [[_ heading]]
  455. (when-let [input-id (state/get-edit-input-id)]
  456. (when-let [current-input (gdom/getElement input-id)]
  457. (let [edit-content (gobj/get current-input "value")
  458. heading-pattern #"^#+\s+"
  459. new-value (cond
  460. (util/safe-re-find heading-pattern edit-content)
  461. (string/replace-first edit-content
  462. heading-pattern
  463. (str heading " "))
  464. :else
  465. (str heading " " (string/triml edit-content)))]
  466. (state/set-edit-content! input-id new-value)))))
  467. (defmethod handle-step :editor/search-page [[_]]
  468. (state/set-editor-show-page-search! true))
  469. (defmethod handle-step :editor/search-page-hashtag [[_]]
  470. (state/set-editor-show-page-search-hashtag! true))
  471. (defmethod handle-step :editor/search-block [[_ type]]
  472. (state/set-editor-show-block-search! true))
  473. (defmethod handle-step :editor/search-template [[_]]
  474. (state/set-editor-show-template-search! true))
  475. (defmethod handle-step :editor/show-input [[_ option]]
  476. (state/set-editor-show-input! option))
  477. (defmethod handle-step :editor/show-date-picker [[_ type]]
  478. (if (and
  479. (contains? #{:scheduled :deadline} type)
  480. (when-let [value (gobj/get (state/get-input) "value")]
  481. (string/blank? value)))
  482. (do
  483. (notification/show! [:div "Please add some content first."] :warning)
  484. (restore-state false))
  485. (state/set-editor-show-date-picker! true)))
  486. (defmethod handle-step :editor/click-hidden-file-input [[_ input-id]]
  487. (when-let [input-file (gdom/getElement "upload-file")]
  488. (.click input-file)))
  489. (defmethod handle-step :default [[type & _args]]
  490. (prn "No handler for step: " type))
  491. (defn handle-steps
  492. [vector format]
  493. (doseq [step vector]
  494. (handle-step step format)))
  495. (defn exec-plugin-simple-command!
  496. [pid {:keys [key label block-id] :as cmd} action]
  497. (let [format (and block-id (:block/format (db-util/pull [:block/uuid block-id])))
  498. inputs (vector (conj action (assoc cmd :pid pid)))]
  499. (handle-steps inputs format)))