Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/ecro/bindings.clj
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
(-> (keymap/make-keymap)
(keymap/define-key ["C-a"] :move-beginning-of-line)
(keymap/define-key ["C-e"] :move-end-of-line)
(keymap/define-key ["C-r"] :isearch-backward)
(keymap/define-key ["C-s"] :isearch-forward)
(keymap/define-key ["C-k"] :kill-line)
(keymap/define-key ["C-z"] :undo)
(keymap/define-key ["C-S-z"] :redo)
Expand Down
13 changes: 13 additions & 0 deletions src/ecro/command.clj
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
[ecro.mode :as mode]
[ecro.notification :as notification]
[ecro.scroll :as scroll]
[ecro.search :as search]
[ecro.state :as state]
[ecro.undo :as undo]))

Expand Down Expand Up @@ -57,6 +58,18 @@
:minibuffer (ecro.minibuffer/prompt-for "Write file: " :write-file)
:key-sequence [])

(= command :isearch-forward)
(assoc editor-state
:isearch (assoc (search/make-isearch :forward)
:start-point (:point buf))
:key-sequence [])

(= command :isearch-backward)
(assoc editor-state
:isearch (assoc (search/make-isearch :backward)
:start-point (:point buf))
:key-sequence [])

(= command :list-buffers)
(state/list-buffers editor-state)

Expand Down
96 changes: 87 additions & 9 deletions src/ecro/key.clj
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,19 @@
[ecro.native :as native]
[ecro.render :as render]
[ecro.scroll :as scroll]
[ecro.search :as search]
[ecro.skk.input :as skk-input]
[ecro.skk.sources :as skk-sources]
[ecro.skk.state :as skk-state]
[ecro.state :as state]))


(def control-modifier 1)


(def alt-modifier 2)


(def shift-modifier 4)


Expand Down Expand Up @@ -164,6 +171,75 @@
:else state)))


(defn- code-point-string
"Convert a valid Unicode code point to a string."
[key-code]
(when (Character/isValidCodePoint key-code)
(String. (Character/toChars key-code))))


(defn- terminal-sentinel-key-code?
"Return true for non-character key codes reserved by the terminal adapter."
[key-code]
(or (<= 1001 key-code 1010)
(<= 2001 key-code 2255)))


(defn- repeat-isearch
[editor-state direction]
(let [isearch (assoc (:isearch editor-state) :direction direction)
buf (search/isearch-repeat isearch
(:current-buffer editor-state)
direction)
isearch (assoc isearch :anchor-point (:point buf))]
(-> editor-state
(assoc :isearch isearch)
(state/assoc-current-buffer buf))))


(defn- handle-isearch-key
"Handle a key event while incremental search is active."
[editor-state key-code modifiers]
(cond
(= key-code 13)
(dissoc editor-state :isearch)

(= key-code 27)
(let [buf (search/isearch-cancel (:isearch editor-state)
(:current-buffer editor-state))]
(-> editor-state
(dissoc :isearch)
(state/assoc-current-buffer buf)))

(= key-code 127)
(let [isearch (search/isearch-delete-char (:isearch editor-state))
buf (search/isearch-execute isearch (:current-buffer editor-state))]
(-> editor-state
(assoc :isearch isearch)
(state/assoc-current-buffer buf)))

(and (= key-code (int \s)) (= modifiers control-modifier))
(repeat-isearch editor-state :forward)

(and (= key-code (int \r)) (= modifiers control-modifier))
(repeat-isearch editor-state :backward)

(terminal-sentinel-key-code? key-code)
editor-state

(and (>= key-code 32)
(zero? (bit-and modifiers (bit-or control-modifier alt-modifier))))
(if-let [text (code-point-string key-code)]
(let [isearch (search/isearch-add-char (:isearch editor-state) text)
buf (search/isearch-execute isearch (:current-buffer editor-state))]
(-> editor-state
(assoc :isearch isearch)
(state/assoc-current-buffer buf)))
editor-state)

:else editor-state))
Comment thread
kqnade marked this conversation as resolved.


(defn- skk-active?
"Return true if SKK minor mode is active in the current buffer."
[editor-state]
Expand Down Expand Up @@ -264,15 +340,17 @@
(defn handle-key
"Handle a key event and return updated state."
[editor-state key-code modifiers]
(if (:minibuffer editor-state)
(handle-minibuffer-key editor-state key-code)
(let [key-str (key-name key-code modifiers)]
(if (and (skk-active? editor-state)
(not (seq (:key-sequence editor-state)))
(not (= "ESC" key-str)))
(or (skk-handle-key editor-state key-str key-code)
(handle-regular-key editor-state key-code modifiers))
(handle-regular-key editor-state key-code modifiers)))))
(if (:isearch editor-state)
(handle-isearch-key editor-state key-code modifiers)
(if (:minibuffer editor-state)
(handle-minibuffer-key editor-state key-code)
(let [key-str (key-name key-code modifiers)]
(if (and (skk-active? editor-state)
(not (seq (:key-sequence editor-state)))
(not (= "ESC" key-str)))
(or (skk-handle-key editor-state key-str key-code)
(handle-regular-key editor-state key-code modifiers))
(handle-regular-key editor-state key-code modifiers))))))


(defn process-event
Expand Down
14 changes: 12 additions & 2 deletions src/ecro/render.clj
Original file line number Diff line number Diff line change
Expand Up @@ -156,8 +156,18 @@
(defn status-line
"Build the status line string from editor state."
[state]
(if-let [mb (:minibuffer state)]
(str (:prompt mb) (:text (:buffer mb)))
(cond
(:isearch state)
(str (if (= :backward (get-in state [:isearch :direction]))
"I-search backward: "
"I-search: ")
(get-in state [:isearch :pattern]))

(:minibuffer state)
(let [mb (:minibuffer state)]
(str (:prompt mb) (:text (:buffer mb))))

:else
(let [buf (:current-buffer state)
name (or (:name buf) "*scratch*")
modified (if (not= (:text buf) (:saved-text buf)) "*" "")
Expand Down
39 changes: 34 additions & 5 deletions src/ecro/search.clj
Original file line number Diff line number Diff line change
Expand Up @@ -35,17 +35,46 @@
(update state :pattern str ch))


(defn isearch-delete-char
"Remove the last character from the i-search pattern."
[state]
(update state :pattern
(fn [^String pattern]
(if (empty? pattern)
pattern
(subs pattern 0 (.offsetByCodePoints pattern (count pattern) -1))))))


(defn isearch-execute
"Execute i-search with current pattern. Returns updated buffer."
[state buf]
(let [pattern (:pattern state)
start-point (or (:start-point state) (:point buf))]
start-point (or (:start-point state) (:point buf))
anchor-point (:anchor-point state)
search-point (if (some? anchor-point)
(if (= :backward (:direction state))
(inc anchor-point)
anchor-point)
start-point)
fallback-point (or anchor-point start-point)]
(if (seq pattern)
(let [result (case (:direction state)
:forward (search-forward (assoc buf :point start-point) pattern)
:backward (search-backward (assoc buf :point start-point) pattern))]
(or result (assoc buf :point start-point)))
buf)))
:forward (search-forward (assoc buf :point search-point) pattern)
:backward (search-backward (assoc buf :point search-point) pattern))]
(or result (assoc buf :point fallback-point)))
(assoc buf :point fallback-point))))


(defn isearch-repeat
"Repeat the current i-search from the current match in direction."
[state buf direction]
(let [pattern (:pattern state)
point (:point buf)
result (when (seq pattern)
(case direction
:forward (search-forward (assoc buf :point (inc point)) pattern)
:backward (search-backward (assoc buf :point point) pattern)))]
(or result buf)))


(defn isearch-cancel
Expand Down
128 changes: 127 additions & 1 deletion test/ecro/key_test.clj
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
[clojure.test :refer :all]
[ecro.bindings :as bindings]
[ecro.buffer :as b]
[ecro.key :as key]))
[ecro.key :as key]
[ecro.render :as render]))


(deftest test-key-name-control-shift-and-control-slash
Expand Down Expand Up @@ -45,6 +46,131 @@
(is (= "Find file: " (get-in new-state [:minibuffer :prompt]))))))


(deftest test-forward-incremental-search-integration
(testing "C-s searches as characters are typed and RET accepts the match"
(let [state {:current-buffer (assoc (b/make-buffer "test")
:text "hello world")
:keymap bindings/default-keymap
:key-sequence []}
started (key/handle-key state (int \s) 1)
with-w (key/handle-key started (int \w) 0)
with-wo (key/handle-key with-w (int \o) 0)
accepted (key/handle-key with-wo 13 0)]
(is (= {:pattern "" :direction :forward :start-point 0}
(:isearch started)))
(is (= 6 (get-in with-w [:current-buffer :point])))
(is (= "wo" (get-in with-wo [:isearch :pattern])))
(is (= "I-search: wo" (render/status-line with-wo)))
(is (= 6 (get-in accepted [:current-buffer :point])))
(is (nil? (:isearch accepted))))))


(deftest test-backward-incremental-search-integration
(testing "C-r searches backward as characters are typed"
(let [state {:current-buffer (assoc (b/make-buffer "test")
:text "foo bar foo"
:point 11)
:keymap bindings/default-keymap
:key-sequence []}
started (key/handle-key state (int \r) 1)
searched (key/handle-key started (int \b) 0)]
(is (= :backward (get-in started [:isearch :direction])))
(is (= "b" (get-in searched [:isearch :pattern])))
(is (= 4 (get-in searched [:current-buffer :point])))
(is (= "I-search backward: b" (render/status-line searched))))))


(deftest test-incremental-search-backspace
(testing "BS removes the last query character and recomputes the match"
(let [state {:current-buffer (assoc (b/make-buffer "test")
:text "hello world")
:keymap bindings/default-keymap
:key-sequence []}
started (key/handle-key state (int \s) 1)
searched (key/handle-key started (int \w) 0)
cleared (key/handle-key searched 127 0)]
(is (= 6 (get-in searched [:current-buffer :point])))
(is (= "" (get-in cleared [:isearch :pattern])))
(is (= 0 (get-in cleared [:current-buffer :point]))))))


(deftest test-incremental-search-cancel
(testing "ESC cancels search and restores the starting point"
(let [state {:current-buffer (assoc (b/make-buffer "test")
:text "hello world"
:point 2)
:keymap bindings/default-keymap
:key-sequence []}
started (key/handle-key state (int \s) 1)
searched (key/handle-key started (int \w) 0)
canceled (key/handle-key searched 27 0)]
(is (= 6 (get-in searched [:current-buffer :point])))
(is (= 2 (get-in canceled [:current-buffer :point])))
(is (nil? (:isearch canceled))))))


(deftest test-incremental-search-non-bmp-character
(testing "a non-BMP code point can be added and removed as one character"
(let [state {:current-buffer (assoc (b/make-buffer "test")
:text "a😀b")
:keymap bindings/default-keymap
:key-sequence []}
started (key/handle-key state (int \s) 1)
searched (key/handle-key started 0x1F600 0)
cleared (key/handle-key searched 127 0)]
(is (= "😀" (get-in searched [:isearch :pattern])))
(is (= 1 (get-in searched [:current-buffer :point])))
(is (= "" (get-in cleared [:isearch :pattern])))
(is (= 0 (get-in cleared [:current-buffer :point]))))))


(deftest test-incremental-search-ignores-terminal-sentinel-codes
(testing "navigation and function key sentinels do not enter the query"
(let [state {:current-buffer (assoc (b/make-buffer "test")
:text "hello world")
:keymap bindings/default-keymap
:key-sequence []}
started (key/handle-key state (int \s) 1)
searched (key/handle-key started (int \w) 0)
after-specials (reduce #(key/handle-key %1 %2 0)
searched
[1001 1004 1005 1010 2001])]
(is (= "w" (get-in after-specials [:isearch :pattern])))
(is (= 6 (get-in after-specials [:current-buffer :point]))))))


(deftest test-incremental-search-classifies-modifiers
(testing "Shift text is accepted while unrelated Ctrl and Alt chords are ignored"
(let [state {:current-buffer (assoc (b/make-buffer "test") :text "W")
:keymap bindings/default-keymap
:key-sequence []}
started (key/handle-key state (int \s) 1)
shifted (key/handle-key started (int \W) key/shift-modifier)
after-chords (-> shifted
(key/handle-key (int \g) 1)
(key/handle-key (int \x) 2))]
(is (= "W" (get-in after-chords [:isearch :pattern])))
(is (= 0 (get-in after-chords [:current-buffer :point]))))))


(deftest test-incremental-search-repeat-controls
(testing "C-s and C-r repeat the query without entering command characters"
(let [state {:current-buffer (assoc (b/make-buffer "test")
:text "foo foo foo")
:keymap bindings/default-keymap
:key-sequence []}
started (key/handle-key state (int \s) 1)
searched (key/handle-key started (int \f) 0)
next-match (key/handle-key searched (int \s) 1)
refined (key/handle-key next-match (int \o) 0)
previous-match (key/handle-key refined (int \r) 1)]
(is (= 4 (get-in next-match [:current-buffer :point])))
(is (= 4 (get-in refined [:current-buffer :point])))
(is (= 0 (get-in previous-match [:current-buffer :point])))
(is (= "fo" (get-in previous-match [:isearch :pattern])))
(is (= :backward (get-in previous-match [:isearch :direction]))))))


(deftest test-handle-key-inserts-non-ascii-character
(testing "a printable Unicode key inserts its character"
(let [state {:current-buffer (b/make-buffer "test")
Expand Down