فهرست منبع

Merge pull request #9553 from logseq/feat/slash-command-for-code-block

feat: add slash command for the code block
Gabriel Horner 2 سال پیش
والد
کامیت
299e2660a9
4فایلهای تغییر یافته به همراه163 افزوده شده و 24 حذف شده
  1. 78 1
      e2e-tests/code-editing.spec.ts
  2. 22 3
      src/main/frontend/commands.cljs
  3. 47 5
      src/main/frontend/components/editor.cljs
  4. 16 15
      src/main/frontend/components/editor.css

+ 78 - 1
e2e-tests/code-editing.spec.ts

@@ -1,6 +1,11 @@
 import { expect } from '@playwright/test'
 import { test } from './fixtures'
-import { createRandomPage, escapeToCodeEditor, escapeToBlockEditor } from './utils'
+import {
+  createRandomPage,
+  escapeToCodeEditor,
+  escapeToBlockEditor,
+  repeatKeyPress,
+} from './utils'
 
 /**
  * NOTE: CodeMirror is a complex library that requires a lot of setup to work.
@@ -241,3 +246,75 @@ test('multi properties with code', async ({ page }) => {
     '```'
   )
 })
+
+test('Select codeblock language', async ({ page }) => {
+  await createRandomPage(page)
+
+  // Open the slash command menu
+  await page.type('textarea >> nth=0', '/code block', { delay: 20 })
+
+  expect(
+    await page.waitForSelector('[data-modal-name="commands"]', {
+      state: 'visible',
+    })
+  ).toBeTruthy()
+
+  // Select `code block` command and open the language dropdown menu
+  await page.press('textarea >> nth=0', 'Enter', { delay: 10 })
+  // wait for the modal to open
+  expect(
+    await page.waitForSelector('[data-modal-name="select-code-block-mode"]', {
+      state: 'visible',
+    })
+  ).toBeTruthy()
+
+  // Select Clojure from the dropdown menu
+  await repeatKeyPress(page, 'ArrowDown', 6)
+  await page.press('textarea >> nth=0', 'Enter', { delay: 10 })
+  // expect the codeblock to be visible
+  expect(await page.waitForSelector('.CodeMirror', { state: 'visible' }))
+
+  // Exit codeblock and return to block edit mode
+  await page.press('.CodeMirror textarea >> nth=0', 'Escape', { delay: 10 })
+
+  expect(await page.inputValue('.block-editor textarea')).toBe(
+    '```clojure\n```'
+  )
+})
+
+test('Select codeblock language while surrounded by text', async ({ page }) => {
+  await createRandomPage(page)
+  await page.type('textarea >> nth=0', 'ABC XYZ', { delay: 20 })
+  await repeatKeyPress(page, 'ArrowLeft', 3)
+
+  // Open the slash command menu
+  await page.type('textarea >> nth=0', '/code block', { delay: 20 })
+
+  expect(
+    await page.waitForSelector('[data-modal-name="commands"]', {
+      state: 'visible',
+    })
+  ).toBeTruthy()
+
+  // Select `code block` command and open the language dropdown menu
+  await page.press('textarea >> nth=0', 'Enter', { delay: 10 })
+  // wait for the modal to open
+  expect(
+    await page.waitForSelector('[data-modal-name="select-code-block-mode"]', {
+      state: 'visible',
+    })
+  ).toBeTruthy()
+
+  // Select Clojure from the dropdown menu
+  await repeatKeyPress(page, 'ArrowDown', 6)
+  await page.press('textarea >> nth=0', 'Enter', { delay: 10 })
+  // expect the codeblock to be visible
+  expect(await page.waitForSelector('.CodeMirror', { state: 'visible' }))
+
+  // Exit codeblock and return to block edit mode
+  await page.press('.CodeMirror textarea >> nth=0', 'Escape', { delay: 10 })
+
+  expect(await page.inputValue('.block-editor textarea')).toBe(
+    'ABC \n```clojure\n```\nXYZ'
+  )
+})

+ 22 - 3
src/main/frontend/commands.cljs

@@ -297,7 +297,12 @@
      ["Embed Youtube timestamp" [[:youtube/insert-timestamp]]]
 
      ["Embed Twitter tweet" [[:editor/input "{{tweet }}" {:last-pattern command-trigger
-                                                          :backward-pos 2}]]]]
+                                                          :backward-pos 2}]]]
+
+     ["Code block" [[:editor/input "```\n```\n" {:type            "block"
+                                                 :backward-pos    5
+                                                 :only-breakline? true}]
+                    [:editor/select-code-block-mode]] "Insert code block"]]
 
     @*extend-slash-commands
     ;; Allow user to modify or extend, should specify how to extend.
@@ -331,7 +336,7 @@
 
 (defn insert!
   [id value
-   {:keys [last-pattern postfix-fn backward-pos end-pattern backward-truncate-number command]
+   {:keys [last-pattern postfix-fn backward-pos end-pattern backward-truncate-number command only-breakline?]
     :as _option}]
   (when-let [input (gdom/getElement id)]
     (let [last-pattern (when-not (= last-pattern :skip-check)
@@ -365,7 +370,8 @@
                                     (and last-pattern
                                          (or (string/ends-with? last-pattern gp-property/colons)
                                              (string/starts-with? last-pattern gp-property/colons)))))))]
-                   (if (and space? (string/starts-with? last-pattern "#[["))
+                   (if (and space? (or (string/starts-with? last-pattern "#[[")
+                                       (string/starts-with? last-pattern "```")))
                      false
                      space?))
           prefix (cond
@@ -381,6 +387,10 @@
 
                    :else
                    (util/replace-last last-pattern orig-prefix value space?))
+          postfix (cond-> postfix
+                          (and only-breakline? postfix
+                               (= (get postfix 0) "\n"))
+                          (string/replace-first "\n" ""))
           new-value (cond
                       (string/blank? postfix)
                       prefix
@@ -692,6 +702,15 @@
       (state/set-timestamp-block! nil)
       (state/set-editor-action! :datepicker))))
 
+(defmethod handle-step :editor/select-code-block-mode [[_]]
+  (-> (p/delay 50)
+      (p/then
+        (fn []
+          (when-let [input (state/get-input)]
+            ;; update action cursor position
+            (state/set-editor-action-data! {:pos (cursor/get-caret-pos input)})
+            (state/set-editor-action! :select-code-block-mode))))))
+
 (defmethod handle-step :editor/click-hidden-file-input [[_ _input-id]]
   (when-let [input-file (gdom/getElement "upload-file")]
     (.click input-file)))

+ 47 - 5
src/main/frontend/components/editor.cljs

@@ -14,6 +14,7 @@
             [frontend.handler.editor.lifecycle :as lifecycle]
             [frontend.handler.page :as page-handler]
             [frontend.handler.paste :as paste-handler]
+            [frontend.search :refer [fuzzy-search]]
             [frontend.mixins :as mixins]
             [frontend.modules.shortcut.core :as shortcut]
             [frontend.state :as state]
@@ -291,12 +292,50 @@
           :item-render (fn [property-value] property-value)
           :class       "black"})))))
 
+(rum/defc code-block-mode-keyup-listener
+  [_q _edit-content last-pos current-pos]
+  (rum/use-effect!
+    (fn []
+      (when (< current-pos last-pos)
+        (state/clear-editor-action!)))
+    [last-pos current-pos])
+  [:<>])
+
+(rum/defc code-block-mode-picker < rum/reactive
+  [id format]
+  (when-let [modes (some->> js/window.CodeMirror (.-modes) (js/Object.keys) (js->clj) (remove #(= "null" %)))]
+    (when-let [input (gdom/getElement id)]
+      (let [pos          (state/get-editor-last-pos)
+            current-pos  (cursor/pos input)
+            edit-content (or (state/sub [:editor/content id]) "")
+            q            (or (editor-handler/get-selected-text)
+                             (gp-util/safe-subs edit-content pos current-pos)
+                             "")
+            matched      (seq (fuzzy-search modes q))
+            matched      (or matched (if (string/blank? q) modes [q]))]
+        [:div
+         (code-block-mode-keyup-listener q edit-content pos current-pos)
+         (ui/auto-complete matched
+                           {:on-chosen   (fn [chosen _click?]
+                                           (state/clear-editor-action!)
+                                           (let [prefix (str "```" chosen)
+                                                 last-pattern (str "```" q)]
+                                             (editor-handler/insert-command! id
+                                               prefix format {:last-pattern last-pattern})
+                                             (commands/handle-step [:codemirror/focus])))
+                            :on-enter    (fn []
+                                           (state/clear-editor-action!)
+                                           (commands/handle-step [:codemirror/focus]))
+                            :item-render (fn [mode _chosen?]
+                                           [:strong mode])
+                            :class       "code-block-mode-picker"})]))))
+
 (rum/defcs input < rum/reactive
-  (rum/local {} ::input-value)
-  (mixins/event-mixin
-   (fn [state]
-     (mixins/on-key-down
-      state
+                   (rum/local {} ::input-value)
+                   (mixins/event-mixin
+                     (fn [state]
+                       (mixins/on-key-down
+                         state
       {;; enter
        13 (fn [state e]
             (let [input-value (get state ::input-value)
@@ -579,6 +618,9 @@
       (= :datepicker action)
       (animated-modal "date-picker" (datetime-comp/date-picker id format nil) false)
 
+      (= :select-code-block-mode action)
+      (animated-modal "select-code-block-mode" (code-block-mode-picker id format) true)
+
       (= :input action)
       (animated-modal "input" (input id
                                      (fn [command m]

+ 16 - 15
src/main/frontend/components/editor.css

@@ -1,13 +1,13 @@
 #audio-record-toolbar {
-    position: fixed;
-    background-color: var(--ls-secondary-background-color);
-    width: 90px;
-    justify-content: left;
-    left: 5px;
-    transition: none;
-    z-index: 9999;
-    padding: 5px 5px 5px 8px;
-    border-radius: 5px;
+  position: fixed;
+  background-color: var(--ls-secondary-background-color);
+  width: 90px;
+  justify-content: left;
+  left: 5px;
+  transition: none;
+  z-index: 9999;
+  padding: 5px 5px 5px 8px;
+  border-radius: 5px;
 }
 
 .editor-wrapper {
@@ -41,7 +41,8 @@
     transform: translateY(calc(-100% - 2rem));
   }
 
-  &[data-modal-name="commands"] {
+  &[data-modal-name="commands"],
+  &[data-modal-name="select-code-block-mode"] {
     @screen sm {
       width: 380px !important;
       max-width: 90vw !important;
@@ -76,11 +77,11 @@ pre {
 }
 
 #time-repeater {
-    width: 135px;
-    
-    @screen sm {
-        min-width: 300px;
-    }
+  width: 135px;
+
+  @screen sm {
+    min-width: 300px;
+  }
 }