├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── settings.gradle.kts
├── gradle.properties
├── .gitignore
├── src
└── main
│ ├── clojure
│ └── fominok
│ │ └── ideahelix
│ │ ├── editor
│ │ ├── action.clj
│ │ ├── util.clj
│ │ ├── registers.clj
│ │ ├── ui.clj
│ │ ├── modification.clj
│ │ ├── jumplist.clj
│ │ └── selection.clj
│ │ ├── core.clj
│ │ ├── search.clj
│ │ ├── keymap.clj
│ │ └── editor.clj
│ ├── kotlin
│ └── fominok
│ │ └── ideahelix
│ │ ├── ModeWidget.kt
│ │ └── Init.kt
│ └── resources
│ └── META-INF
│ ├── plugin.xml
│ └── pluginIcon.svg
├── gradlew.bat
├── README.md
├── gradlew
├── ideahelix_light.svg
├── ideahelix_dark.svg
└── LICENSE
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fominok/ideahelix/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | mavenCentral()
4 | gradlePluginPortal()
5 | }
6 | }
7 |
8 | rootProject.name = "ideahelix"
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Opt-out flag for bundling Kotlin standard library -> https://jb.gg/intellij-platform-kotlin-stdlib
2 | kotlin.stdlib.default.dependency=false
3 | # Enable Gradle Configuration Cache -> https://docs.gradle.org/current/userguide/configuration_cache.html
4 | org.gradle.configuration-cache=true
5 | # Enable Gradle Build Cache -> https://docs.gradle.org/current/userguide/build_cache.html
6 | org.gradle.caching=true
7 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .gradle
2 | build/
3 | !gradle/wrapper/gradle-wrapper.jar
4 | !**/src/main/**/build/
5 | !**/src/test/**/build/
6 |
7 | ### IntelliJ IDEA ###
8 | .intellijPlatform
9 | .idea/modules.xml
10 | .idea/jarRepositories.xml
11 | .idea/compiler.xml
12 | .idea/libraries/
13 | *.iws
14 | *.iml
15 | *.ipr
16 | out/
17 | !**/src/main/**/out/
18 | !**/src/test/**/out/
19 |
20 | ### Eclipse ###
21 | .apt_generated
22 | .classpath
23 | .factorypath
24 | .project
25 | .settings
26 | .springBeans
27 | .sts4-cache
28 | bin/
29 | !**/src/main/**/bin/
30 | !**/src/test/**/bin/
31 |
32 | ### NetBeans ###
33 | /nbproject/private/
34 | /nbbuild/
35 | /dist/
36 | /nbdist/
37 | /.nb-gradle/
38 |
39 | ### VS Code ###
40 | .vscode/
41 |
42 | ### Mac OS ###
43 | .DS_Store
44 | .clj-kondo/.cache/v1
45 | .idea
46 | .lsp/.cache
47 |
--------------------------------------------------------------------------------
/src/main/clojure/fominok/ideahelix/editor/action.clj:
--------------------------------------------------------------------------------
1 | ;; This Source Code Form is subject to the terms of the Mozilla Public
2 | ;; License, v. 2.0. If a copy of the MPL was not distributed with this
3 | ;; file, You can obtain one at https://mozilla.org/MPL/2.0/.
4 |
5 | (ns fominok.ideahelix.editor.action
6 | (:import
7 | (com.intellij.openapi.actionSystem
8 | ActionManager
9 | ActionPlaces
10 | AnActionEvent)
11 | (com.intellij.openapi.editor.impl
12 | EditorImpl)))
13 |
14 |
15 | (defn actions
16 | [^EditorImpl editor & action-names]
17 | (let [data-context (.getDataContext editor)]
18 | (doseq [action-name action-names]
19 | (let [action (.getAction (ActionManager/getInstance) action-name)]
20 | (.actionPerformed
21 | action
22 | (AnActionEvent/createFromDataContext
23 | ActionPlaces/KEYBOARD_SHORTCUT nil data-context))))))
24 |
--------------------------------------------------------------------------------
/src/main/clojure/fominok/ideahelix/editor/util.clj:
--------------------------------------------------------------------------------
1 | ;; This Source Code Form is subject to the terms of the Mozilla Public
2 | ;; License, v. 2.0. If a copy of the MPL was not distributed with this
3 | ;; file, You can obtain one at https://mozilla.org/MPL/2.0/.
4 |
5 | (ns fominok.ideahelix.editor.util)
6 |
7 |
8 | (defmacro when-let*
9 | ([bindings & body]
10 | (if (seq bindings)
11 | `(when-let [~(first bindings) ~(second bindings)]
12 | (when-let* ~(drop 2 bindings) ~@body))
13 | `(do ~@body))))
14 |
15 |
16 | (defn deep-merge
17 | [& maps]
18 | (reduce (fn [m1 m2]
19 | (merge-with (fn [v1 v2]
20 | (if (and (map? v1) (map? v2))
21 | (deep-merge v1 v2)
22 | v2))
23 | m1 m2))
24 | maps))
25 |
26 |
27 | (defn get-caret-contents
28 | [document caret]
29 | (.getText document (.getSelectionRange caret)))
30 |
31 |
32 | (defn get-editor-height
33 | [editor]
34 | (let [editor-height-px (.. editor getScrollingModel getVisibleArea getHeight)
35 | line-height-px (.getLineHeight editor)]
36 | (int (quot editor-height-px line-height-px))))
37 |
38 |
39 | (defn printable-char?
40 | [c]
41 | (let [block (java.lang.Character$UnicodeBlock/of c)]
42 | (and (not (Character/isISOControl c))
43 | (not= c java.awt.event.KeyEvent/CHAR_UNDEFINED)
44 | (some? block)
45 | (not= block java.lang.Character$UnicodeBlock/SPECIALS))))
46 |
--------------------------------------------------------------------------------
/src/main/kotlin/fominok/ideahelix/ModeWidget.kt:
--------------------------------------------------------------------------------
1 | package fominok.ideahelix
2 |
3 | import com.intellij.openapi.project.Project
4 | import com.intellij.openapi.wm.StatusBarWidget
5 | import com.intellij.openapi.wm.StatusBarWidgetFactory
6 |
7 | class ModeWidget : StatusBarWidgetFactory {
8 | override fun getId(): String {
9 | return ModePanel.ID
10 | }
11 |
12 | override fun getDisplayName(): String {
13 | return "Ideahelix Mode Widget"
14 | }
15 |
16 | override fun isAvailable(project: Project): Boolean {
17 | return true
18 | }
19 |
20 | override fun createWidget(project: Project): StatusBarWidget {
21 | return ModePanel()
22 | }
23 |
24 | override fun isEnabledByDefault(): Boolean {
25 | return true
26 | }
27 | }
28 |
29 | class ModePanel : StatusBarWidget.TextPresentation, StatusBarWidget {
30 |
31 | private var text: String = "Loading..."
32 |
33 | fun setText(text: String) {
34 | this.text = text
35 | }
36 | override fun getAlignment(): Float {
37 | return 0.0f
38 | }
39 |
40 | override fun getText(): String {
41 | return this.text
42 | }
43 |
44 | override fun getTooltipText(): String {
45 | return "Ideahelix mode"
46 | }
47 |
48 | override fun ID(): String {
49 | return ID
50 | }
51 |
52 | override fun getPresentation(): StatusBarWidget.WidgetPresentation {
53 | return this
54 | }
55 |
56 | companion object {
57 | @JvmField
58 | val ID: String = "IdeahelixModeWidget"
59 | }
60 |
61 |
62 | }
--------------------------------------------------------------------------------
/src/main/clojure/fominok/ideahelix/editor/registers.clj:
--------------------------------------------------------------------------------
1 | ;; This Source Code Form is subject to the terms of the Mozilla Public
2 | ;; License, v. 2.0. If a copy of the MPL was not distributed with this
3 | ;; file, You can obtain one at https://mozilla.org/MPL/2.0/.
4 |
5 | (ns fominok.ideahelix.editor.registers
6 | (:require
7 | [fominok.ideahelix.editor.selection :refer :all]))
8 |
9 |
10 | (defn copy-to-register
11 | [registers editor document & {:keys [register] :or {register \"}}]
12 | (let [model (.getCaretModel editor)
13 | strings
14 | (let [strings* (transient [])]
15 | (.runForEachCaret
16 | model
17 | (fn [caret] (conj! strings* (.getText document (.getSelectionRange caret)))))
18 | (persistent! strings*))]
19 | (assoc registers register strings)))
20 |
21 |
22 | (defn paste-register
23 | [registers editor document & {:keys [register select] :or {register \" select false}}]
24 | (let [register-contents (get registers register)
25 | strings (concat register-contents (some-> (last register-contents) repeat))
26 | pairs (map (fn [caret string] [(ihx-selection document caret) string])
27 | (.. editor getCaretModel getAllCarets)
28 | strings)]
29 | (when (not (empty? strings))
30 | (doseq [[selection string] pairs
31 | :let [pos (min (.getTextLength document)
32 | (inc (max (:anchor selection) (:offset selection))))]]
33 | (.insertString document pos string)
34 | (-> selection
35 | (assoc :anchor pos)
36 | (assoc :offset (dec (+ pos (count string))))
37 | (ihx-apply-selection! document))))))
38 |
--------------------------------------------------------------------------------
/src/main/resources/META-INF/plugin.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | fominok.ideahelix
5 |
6 |
8 | IdeaHelix
9 |
10 |
11 | Evgeny Fomin
12 |
13 |
16 |
19 |
20 |
22 | com.intellij.modules.platform
23 |
24 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/src/main/clojure/fominok/ideahelix/editor/ui.clj:
--------------------------------------------------------------------------------
1 | ;; This Source Code Form is subject to the terms of the Mozilla Public
2 | ;; License, v. 2.0. If a copy of the MPL was not distributed with this
3 | ;; file, You can obtain one at https://mozilla.org/MPL/2.0/.
4 |
5 | (ns fominok.ideahelix.editor.ui
6 | (:require
7 | [clojure.string :as str]
8 | [fominok.ideahelix.editor.util :refer [when-let*]])
9 | (:import
10 | (com.intellij.openapi.editor
11 | CaretVisualAttributes
12 | CaretVisualAttributes$Weight)
13 | (com.intellij.openapi.wm
14 | WindowManager)
15 | (com.intellij.ui
16 | JBColor)
17 | (fominok.ideahelix
18 | ModePanel)))
19 |
20 |
21 | (defn update-mode-panel!
22 | [project editor-state]
23 | (let [id (ModePanel/ID)
24 | mode-text (str/upper-case (name (or (:mode editor-state)
25 | :normal)))
26 | widget-text
27 | (str
28 | (when-let [prefix (:prefix editor-state)] (format "(%s) " (apply str prefix)))
29 | mode-text)]
30 | (when-let* [status-bar (.. WindowManager getInstance (getStatusBar project))
31 | widget (.getWidget status-bar id)]
32 | (.setText widget widget-text)
33 | (.updateWidget status-bar id))))
34 |
35 |
36 | (defn highlight-primary-caret
37 | [editor event]
38 | (let [primary-caret (.. editor getCaretModel getPrimaryCaret)
39 | primary-attributes
40 | (CaretVisualAttributes. JBColor/GRAY CaretVisualAttributes$Weight/HEAVY)
41 | secondary-attributes
42 | (CaretVisualAttributes. JBColor/BLACK CaretVisualAttributes$Weight/HEAVY)
43 | caret (.getCaret event)]
44 | (.setVisualAttributes caret
45 | (if (= caret primary-caret)
46 | primary-attributes
47 | secondary-attributes))))
48 |
--------------------------------------------------------------------------------
/src/main/clojure/fominok/ideahelix/core.clj:
--------------------------------------------------------------------------------
1 | ;; This Source Code Form is subject to the terms of the Mozilla Public
2 | ;; License, v. 2.0. If a copy of the MPL was not distributed with this
3 | ;; file, You can obtain one at https://mozilla.org/MPL/2.0/.
4 |
5 | (ns fominok.ideahelix.core
6 | (:require
7 | [cider.nrepl :refer (cider-nrepl-handler)]
8 | [fominok.ideahelix.editor :refer [handle-editor-event state-atom quit-insert-mode]]
9 | [fominok.ideahelix.editor.selection :refer :all]
10 | [fominok.ideahelix.editor.ui :as ui]
11 | [nrepl.server :refer [start-server]])
12 | (:import
13 | (com.intellij.openapi.editor
14 | Editor)
15 | (com.intellij.openapi.editor.event
16 | CaretListener)
17 | (com.intellij.openapi.editor.impl
18 | EditorComponentImpl)))
19 |
20 |
21 | (set! *warn-on-reflection* true)
22 |
23 |
24 | (defn push-event
25 | [project focus-owner event]
26 | (boolean
27 | (when (instance? EditorComponentImpl focus-owner)
28 | (let [editor (.getEditor ^EditorComponentImpl focus-owner)]
29 | (when-not (.isOneLineMode editor)
30 | (handle-editor-event project editor event))))))
31 |
32 |
33 | (defn- caret-listener
34 | [editor]
35 | (reify CaretListener
36 | (caretPositionChanged
37 | [_ event]
38 | (ui/highlight-primary-caret editor event))))
39 |
40 |
41 | (defn focus-editor
42 | [project ^Editor editor]
43 | (let [project-state (or (get @state-atom project) {:mode :normal})
44 | document (.getDocument editor)]
45 | (when-not (get-in project-state [:caret-listeners editor])
46 | (let [listener (caret-listener editor)
47 | _ (.. editor getCaretModel (addCaretListener listener))]
48 | (swap! state-atom assoc-in [project :caret-listeners editor] listener)))
49 | (ui/update-mode-panel! project project-state)
50 | (when (= (:mode project-state) :normal)
51 | (.. editor getCaretModel
52 | (runForEachCaret (fn [caret]
53 | (-> (ihx-selection document caret)
54 | (ihx-apply-selection! document))))))))
55 |
56 |
57 | (defonce -server (start-server :port 7888 :handler cider-nrepl-handler))
58 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/src/main/clojure/fominok/ideahelix/editor/modification.clj:
--------------------------------------------------------------------------------
1 | ;; This Source Code Form is subject to the terms of the Mozilla Public
2 | ;; License, v. 2.0. If a copy of the MPL was not distributed with this
3 | ;; file, You can obtain one at https://mozilla.org/MPL/2.0/.
4 |
5 | (ns fominok.ideahelix.editor.modification
6 | (:require
7 | [fominok.ideahelix.editor.selection :refer :all]
8 | [fominok.ideahelix.editor.util
9 | :refer [get-caret-contents]])
10 | (:import
11 | (com.intellij.openapi.command
12 | CommandProcessor)
13 | (com.intellij.openapi.command.impl
14 | FinishMarkAction
15 | StartMarkAction)))
16 |
17 |
18 | (defn finish-undo
19 | [project editor start-mark]
20 | (.. CommandProcessor getInstance
21 | (executeCommand
22 | project
23 | (fn [] (FinishMarkAction/finish project editor start-mark))
24 | "IHx: Insertion"
25 | nil)))
26 |
27 |
28 | (defn delete-selection-contents
29 | [{:keys [anchor offset] :as selection} document]
30 | (let [[start end] (sort [anchor offset])]
31 | (.deleteString document start (min (.getTextLength document) (inc end)))
32 | (assoc selection :anchor start :offset start)))
33 |
34 |
35 | (defn start-undo
36 | [project editor]
37 | (let [return (volatile! nil)]
38 | (.. CommandProcessor getInstance
39 | (executeCommand
40 | project
41 | (fn []
42 | (let [start (StartMarkAction/start editor project "IHx: Insertion")]
43 | (vreset! return start)))
44 | "IHx: Insertion"
45 | nil))
46 | @return))
47 |
48 |
49 | (defn replace-selections
50 | [project-state project editor document & {:keys [register] :or {register \"}}]
51 | (let [start (start-undo project editor)
52 | carets (.. editor getCaretModel getAllCarets)
53 | register-contents
54 | (doall (for [caret carets
55 | :let [text (get-caret-contents document caret)]]
56 | (do
57 | (-> (ihx-selection document caret)
58 | (delete-selection-contents document)
59 | (ihx-apply-selection! document))
60 | text)))
61 | pre-selections (dump-drop-selections! editor document)]
62 | (-> project-state
63 | (assoc-in [:registers register] register-contents)
64 | (assoc-in [:per-editor editor :pre-selections] pre-selections)
65 | (assoc-in [:per-editor editor :mark-action] start)
66 | (assoc :mode :insert)
67 | (assoc :insertion-kind :prepend)
68 | (dissoc :prefix)
69 | (assoc :debounce true))))
70 |
71 |
72 | (defn delete-selections
73 | [project-state editor document & {:keys [register] :or {register \"}}]
74 | (let [carets (.. editor getCaretModel getAllCarets)
75 | register-contents
76 | (doall (for [caret carets
77 | :let [text (get-caret-contents document caret)]]
78 | (do
79 | (-> (ihx-selection document caret)
80 | (delete-selection-contents document)
81 | (ihx-apply-selection! document))
82 | text)))]
83 | (-> project-state
84 | (assoc-in [:registers register] register-contents)
85 | (assoc :mode :normal)
86 | (assoc :prefix nil))))
87 |
--------------------------------------------------------------------------------
/src/main/clojure/fominok/ideahelix/editor/jumplist.clj:
--------------------------------------------------------------------------------
1 | ;; This Source Code Form is subject to the terms of the Mozilla Public
2 | ;; License, v. 2.0. If a copy of the MPL was not distributed with this
3 | ;; file, You can obtain one at https://mozilla.org/MPL/2.0/.
4 |
5 | (ns fominok.ideahelix.editor.jumplist
6 | (:require
7 | [fominok.ideahelix.editor.selection :refer :all])
8 | (:import
9 | (com.intellij.openapi.fileEditor
10 | FileEditorManager)
11 | (com.intellij.openapi.fileEditor.impl
12 | EditorWindow
13 | FileEditorManagerImpl
14 | FileEditorOpenOptions)
15 | (com.intellij.openapi.vfs
16 | VirtualFile)
17 | (com.intellij.util.ui
18 | UIUtil)))
19 |
20 |
21 | (defn- serialize-selection
22 | [selection]
23 | (dissoc selection :caret :in-append))
24 |
25 |
26 | (defn jumplist-add
27 | [project project-state]
28 | (let [editor (.. (FileEditorManager/getInstance project) getSelectedTextEditor)
29 | document (.getDocument editor)
30 | {:keys [stack pointer] :or {pointer 0}} (:jumplist project-state)
31 | model (.getCaretModel editor)
32 | primary-caret (.getPrimaryCaret model)
33 | secondary-carets (filter (partial not= primary-caret) (.getAllCarets model))
34 | new-stack (into [] (take pointer stack))
35 | serialize (comp serialize-selection (partial ihx-selection document))]
36 | (-> project-state
37 | (assoc-in
38 | [:jumplist :stack]
39 | (conj new-stack
40 | {:file (.getVirtualFile editor)
41 | :primary-caret (serialize primary-caret)
42 | :secondary-carets (doall (map serialize secondary-carets))}))
43 | (assoc-in [:jumplist :pointer] (inc pointer)))))
44 |
45 |
46 | (defn- open-file-current-window
47 | [project file]
48 | (let [manager (FileEditorManager/getInstance project)
49 | window (.getCurrentWindow manager)]
50 | (.openFile ^FileEditorManagerImpl manager ^VirtualFile file ^EditorWindow window (FileEditorOpenOptions.))
51 | (UIUtil/invokeLaterIfNeeded
52 | (fn [] (.. manager getSelectedTextEditor getContentComponent requestFocus)))))
53 |
54 |
55 | (defn- jumplist-apply!
56 | [project-state project new-pointer]
57 | (let [stack (get-in project-state [:jumplist :stack])
58 | {:keys [primary-caret secondary-carets file]} (nth stack new-pointer)
59 | _ (open-file-current-window project file)
60 | editor (.. (FileEditorManager/getInstance project) getSelectedTextEditor)
61 | document (.getDocument editor)
62 | model (.getCaretModel editor)
63 | primary (.getPrimaryCaret model)
64 | text-length (.getTextLength document)]
65 | (-> (->IhxSelection primary
66 | (:anchor primary-caret)
67 | (:offset primary-caret)
68 | false)
69 | (ihx-apply-selection! document))
70 | (doseq [{:keys [anchor offset]} secondary-carets]
71 | (when-let [caret (.addCaret model (.offsetToVisualPosition editor (min text-length offset)))]
72 | (-> (->IhxSelection caret anchor offset false)
73 | (ihx-apply-selection! document))))
74 | (assoc-in project-state [:jumplist :pointer] new-pointer)))
75 |
76 |
77 | (defn jumplist-backward!
78 | [project-state project]
79 | (let [{:keys [pointer] :or {pointer 0}} (:jumplist project-state)
80 | new-pointer (dec pointer)]
81 | (if (>= new-pointer 0)
82 | (jumplist-apply! project-state project new-pointer)
83 | project-state)))
84 |
85 |
86 | (defn jumplist-forward!
87 | [project-state project]
88 | (let [{:keys [pointer stack] :or {pointer 0}} (:jumplist project-state)
89 | new-pointer (inc pointer)]
90 | (if (< pointer (count stack))
91 | (assoc-in (jumplist-apply! project-state project pointer)
92 | [:jumplist :pointer] new-pointer)
93 | project-state)))
94 |
--------------------------------------------------------------------------------
/src/main/resources/META-INF/pluginIcon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
120 |
--------------------------------------------------------------------------------
/src/main/kotlin/fominok/ideahelix/Init.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * This Source Code Form is subject to the terms of the Mozilla Public
3 | * License, v. 2.0. If a copy of the MPL was not distributed with this
4 | * file, You can obtain one at https://mozilla.org/MPL/2.0/.
5 | */
6 |
7 | package fominok.ideahelix
8 |
9 | import clojure.java.api.Clojure
10 | import clojure.lang.IFn
11 | import com.intellij.ide.IdeEventQueue
12 | import com.intellij.openapi.application.ApplicationManager
13 | import com.intellij.openapi.editor.Editor
14 | import com.intellij.openapi.editor.EditorFactory
15 | import com.intellij.openapi.editor.ex.EditorEventMulticasterEx
16 | import com.intellij.openapi.editor.ex.EditorSettingsExternalizable
17 | import com.intellij.openapi.editor.ex.FocusChangeListener
18 | import com.intellij.openapi.fileEditor.FileEditorManager
19 | import com.intellij.openapi.fileEditor.FileEditorManagerListener
20 | import com.intellij.openapi.fileEditor.TextEditor
21 | import com.intellij.openapi.project.Project
22 | import com.intellij.openapi.startup.ProjectActivity
23 | import com.intellij.openapi.vfs.VirtualFile
24 | import java.awt.KeyboardFocusManager
25 | import java.awt.event.KeyEvent
26 |
27 | class Init : ProjectActivity {
28 | override suspend fun execute(project: Project) {
29 | val pushEvent: IFn
30 | val focusEditor: IFn
31 |
32 | val settings = EditorSettingsExternalizable.getInstance()
33 | ApplicationManager.getApplication().invokeAndWait {
34 | settings.isVariableInplaceRenameEnabled = false;
35 | }
36 |
37 | // Per https://plugins.jetbrains.com/docs/intellij/plugin-class-loaders.html#using-serviceloader:
38 | val currentThread = Thread.currentThread()
39 | val originalClassLoader = currentThread.contextClassLoader
40 | val pluginClassLoader = javaClass.classLoader
41 | try {
42 | currentThread.contextClassLoader = pluginClassLoader
43 |
44 | val require = Clojure.`var`("clojure.core", "require")
45 | require.invoke(Clojure.read("fominok.ideahelix.core"))
46 |
47 | pushEvent = Clojure.`var`("fominok.ideahelix.core", "push-event") as IFn
48 | focusEditor = Clojure.`var`("fominok.ideahelix.core", "focus-editor") as IFn
49 | } finally {
50 | currentThread.contextClassLoader = originalClassLoader
51 | }
52 |
53 | val fileEditorManager = FileEditorManager.getInstance(project)
54 | val applicationManager = ApplicationManager.getApplication()
55 |
56 | fileEditorManager.openFiles.forEach {
57 | applicationManager.invokeLater({
58 | val editor = (fileEditorManager.getEditors(it).firstOrNull() as TextEditor).editor
59 | focusEditor.invoke(project, editor)
60 | })
61 | }
62 |
63 | project.messageBus.connect().subscribe(FileEditorManagerListener.FILE_EDITOR_MANAGER, object: FileEditorManagerListener {
64 | override fun fileOpened(source: FileEditorManager, file: VirtualFile) {
65 | super.fileOpened(source, file)
66 | applicationManager.invokeLater({
67 | val editor: TextEditor = source.getEditors(file).firstOrNull() as TextEditor
68 | focusEditor.invoke(project, editor.editor)
69 | })
70 | }
71 | })
72 |
73 | val caster: EditorEventMulticasterEx = EditorFactory.getInstance().eventMulticaster as EditorEventMulticasterEx;
74 | caster.addFocusChangeListener(object : FocusChangeListener {
75 | override fun focusGained(editor: Editor) {
76 | super.focusGained(editor)
77 | applicationManager.invokeLater({
78 | focusEditor.invoke(project, editor)
79 | })
80 | }
81 | }, project)
82 |
83 |
84 | IdeEventQueue.getInstance().addDispatcher({
85 | if (it is KeyEvent) {
86 | val focusOwner = KeyboardFocusManager.getCurrentKeyboardFocusManager().focusOwner
87 | pushEvent.invoke(project, focusOwner, it) as Boolean
88 | } else {
89 | false
90 | }
91 | }, project)
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | [JetBrains IDEs'](https://www.jetbrains.com/ides/) plugin for a
12 | [Helix](https://helix-editor.com)-like experience.
13 |
14 | 🚧 _Work in progress! See issues for more details._ 🚧
15 |
16 | [LSP](https://github.com/microsoft/language-server-protocol) is not a silver bullet, and
17 | there are still some projects and workflows that are better served by more heavyweight
18 | solutions, with JB IDEs arguably being some of the best in that regard. Unfortunately,
19 | this often forces us to leave an environment like Helix, which feels more like a right
20 | hand than just an editor. The goal of this project is to bridge the gap between the
21 | tooling provided by JetBrains and the editing model of Helix.
22 |
23 | ## Goals
24 | - Helix users and their muscle memory should feel at home with no need to re-learn
25 | everything.
26 |
27 | ## Non-Goals
28 | - **1:1 Emulation** – Achieving a perfect, one-to-one emulation of Helix is not the
29 | goal, at least for the foreseeable future. Reuse is preferred, and differences in
30 | caret implementations may lead to edge cases. If you're looking for the complete Helix
31 | experience, there is Helix.
32 | - **Plugin Best Practices Compliance** – Stability isn't guaranteed, as the plugin
33 | interrupts keyboard events with the author's limited knowledge of JetBrains IDEs and
34 | it likely doesn't follow plugin development best practices. Reaching ~150k lines while
35 | pursuing those, as IdeaVim does, is something to avoid. Contributions are welcome, as
36 | basic compatibility or UX improvements are always possible without overdoing it.
37 | - **Mouse/Menu interactions**: The primary focus is on keyboard interactions. While
38 | executing actions through other means is supported, there are no guarantees that carets
39 | or selections will behave in a specific manner.
40 |
41 | ## Emulating Helix Carets
42 |
43 | In Helix, carets are visually represented as blocks, but these blocks are not
44 | just indicators -- they are actual one-character selections. This means the visual
45 | representation directly corresponds to the underlying behavior: replacing or deleting a
46 | character removes it from the document and places it into a register, just like a yank
47 | operation. This leads to the first rule:
48 |
49 | **Selections are always at least one character long, with a line caret positioned either
50 | before the first character or before the last.**
51 |
52 | To illustrate this, let's introduce a legend:
53 |
54 | - `|` -- Line caret (default in Idea and many other non-TUI editors)
55 | - `|x|` -- Block caret highlighting character `x` (as in Helix)
56 | - `║` -- Selection boundaries
57 |
58 | Here’s how selecting the word `hello` appears in Helix compared to IdeaHelix, with spaces
59 | preserved to reflect caret positioning accurately:
60 |
61 | ```
62 | Forward-facing selection:
63 | Hx : ║h e l l|o|
64 | IHx: ║h e l l|o║
65 |
66 | Backward-facing selection:
67 | Hx : |h|e l l o║
68 | IHx: |h e l l o║
69 | ```
70 |
71 | **A caret can only be in one of two positions: at the start of the selection or just
72 | before the last selected character.**
73 |
74 | ### Edge Case: Empty Buffer
75 |
76 | If the buffer is empty, no selection exists—this is the only exception to the first rule.
77 | As soon as text is inserted, the standard behavior applies.
78 |
79 | ### Insertion mode
80 |
81 | In Helix, a caret is effectively a one-character selection outside of insert mode and
82 | behaves like a block caret within it. IdeaHelix ensures text is inserted at the same
83 | position as in Helix but uses Idea’s native visual representation -- a line caret with
84 | selections turned off.
85 |
86 | Insert mode in IdeaHelix bypasses its own handlers, temporarily handing control back
87 | to Idea. This simplifies insert mode behavior, allowing Idea’s features to work without
88 | interference. As a bonus, this also enables the use of native keystroke configurations
89 | within insert mode.
90 |
91 | In Helix, selections remain active and may expand when entering insert mode via prepend
92 | or append. However, due to Idea’s limitations, IdeaHelix must disable selections while
93 | typing. This ensures seamless integration with Idea’s completion system, which relies
94 | on the selection start rather than the actual caret position. Once insert mode ends,
95 | selections are restored to match how they would appear in Helix.
96 |
97 | ```
98 | Append:
99 | Hx : ║h e l l o| |w o r l d
100 | IHx: h e l l o| w o r l d
101 |
102 | Prepend:
103 | Hx : |h|e l l o║ w o r l d
104 | IHx: |h e l l o w o r l d
105 | ```
106 |
107 | ### Exiting Insertion Mode
108 |
109 | When leaving insertion mode, selections reappear as follows:
110 |
111 | ```
112 | Exiting append:
113 | Hx : ║h e l l|o| w o r l d
114 | IHx: ║h e l l|o║ w o r l d
115 |
116 | Exiting prepend (no change in either case):
117 | Hx : |h|e l l o║ w o r l d
118 | IHx: |h e l l o║ w o r l d
119 | ```
120 |
121 | ## Acknowledgments
122 | - [Kakoune](https://kakoune.org) – for ruining my life by making every other editing
123 | style unbearable.
124 | - [Iosevka](https://typeof.net/Iosevka/) – for being the font of choice, both in the
125 | logo and for daily use (licensed under the
126 | [SIL Open Font License](https://opensource.org/licenses/OFL-1.1)).
127 |
--------------------------------------------------------------------------------
/src/main/clojure/fominok/ideahelix/search.clj:
--------------------------------------------------------------------------------
1 | (ns fominok.ideahelix.search
2 | (:import
3 | (com.intellij.openapi.application
4 | ModalityState
5 | ReadAction)
6 | (com.intellij.openapi.editor
7 | Editor)
8 | (com.intellij.openapi.fileEditor
9 | FileEditorManager)
10 | (com.intellij.openapi.project
11 | Project
12 | ProjectUtil
13 | ProjectUtilCore)
14 | (com.intellij.openapi.ui.popup
15 | JBPopupFactory)
16 | (com.intellij.psi.codeStyle
17 | NameUtil)
18 | (com.intellij.psi.search
19 | FilenameIndex
20 | GlobalSearchScope)
21 | (com.intellij.ui.components
22 | JBList
23 | JBScrollPane)
24 | (com.intellij.util
25 | Alarm
26 | Alarm$ThreadToUse)
27 | (com.intellij.util.concurrency
28 | AppExecutorUtil)
29 | (java.awt
30 | BorderLayout
31 | Dimension)
32 | (java.awt.event
33 | ActionEvent)
34 | (javax.swing
35 | AbstractAction
36 | DefaultListModel
37 | JPanel
38 | JTextField
39 | KeyStroke
40 | ListSelectionModel)
41 | (javax.swing.event
42 | DocumentListener)))
43 |
44 |
45 | (defn filter-list
46 | [alarm ^JTextField input ^JBList list ^DefaultListModel model files]
47 | (.cancelAllRequests alarm)
48 | (.addRequest
49 | alarm
50 | (fn []
51 | (let [query (.trim (.getText input))
52 | matcher (.. (NameUtil/buildMatcher (str "*" query)) build)
53 | scored-items (map (fn [[path _]]
54 | {:score (.matchingDegree matcher path)
55 | :path path})
56 | files)
57 | items
58 | (->> scored-items
59 | (sort-by :score >)
60 | (take 20)
61 | (map :path))]
62 |
63 | (.clear model)
64 | (doseq [item items]
65 | (.addElement model item))
66 | (when-not (empty? items)
67 | (.setSelectedIndex list 0))))
68 | 100
69 | true))
70 |
71 |
72 | (defn relativize-path
73 | [project path]
74 | (let [base-path (.getPath (ProjectUtil/guessProjectDir project))]
75 | (if (and (.startsWith path base-path) (not= path base-path))
76 | (.substring path (inc (count base-path)))
77 | path)))
78 |
79 |
80 | (defn get-filenames!
81 | [project result]
82 | (.. (^[Callable] ReadAction/nonBlocking
83 | (fn []
84 | (into {} (keep (fn [name]
85 | (when-let [file (first (FilenameIndex/getVirtualFilesByName name (GlobalSearchScope/projectScope project)))]
86 | [(relativize-path project (.getPath file)) file]))
87 | (FilenameIndex/getAllFilenames project)))))
88 | (finishOnUiThread
89 | (ModalityState/any)
90 | #(vreset! result %))
91 | (submit (AppExecutorUtil/getAppExecutorService))))
92 |
93 |
94 | (defn search-file-name
95 | [project ^Editor parent]
96 | (let [files (volatile! {})
97 | alarm (Alarm. Alarm$ThreadToUse/POOLED_THREAD project)
98 | model (DefaultListModel.)
99 | list (doto (JBList. model)
100 | (.setSelectionMode ListSelectionModel/SINGLE_SELECTION)
101 | (.setSelectedIndex 0))
102 | input (JTextField.)
103 | panel (doto (JPanel. (BorderLayout. 0 5))
104 | (.add input BorderLayout/NORTH)
105 | (.add (JBScrollPane. list) BorderLayout/CENTER)
106 | (.setPreferredSize (Dimension. 600 400)))
107 | popup (-> (JBPopupFactory/getInstance)
108 | (.createComponentPopupBuilder panel input)
109 | (.setRequestFocus true)
110 | (.setTitle "Find File")
111 | (.setMovable true)
112 | (.setResizable true)
113 | (.createPopup))]
114 |
115 | (get-filenames! project files)
116 |
117 | (doto (.getInputMap input)
118 | (.put (KeyStroke/getKeyStroke "control N") "selectNext")
119 | (.put (KeyStroke/getKeyStroke "control P") "selectPrevious"))
120 | (doto (.getActionMap input)
121 | (.put "selectNext"
122 | (proxy [AbstractAction] []
123 | (actionPerformed
124 | [^ActionEvent _]
125 | (let [i (min (inc (.getSelectedIndex list)) (dec (.. list (getModel) (getSize))))]
126 | (.setSelectedIndex list i)
127 | (.ensureIndexIsVisible list i)))))
128 | (.put "selectPrevious"
129 | (proxy [AbstractAction] []
130 | (actionPerformed
131 | [^ActionEvent _]
132 | (let [i (max (dec (.getSelectedIndex list)) 0)]
133 | (.setSelectedIndex list i)
134 | (.ensureIndexIsVisible list i))))))
135 |
136 | (.addActionListener input
137 | (proxy [java.awt.event.ActionListener] []
138 | (actionPerformed
139 | [^ActionEvent _]
140 | (let [selected (.getSelectedValue list)]
141 | (.cancel popup)
142 | (when-let [file (get @files selected)]
143 | (.openFile (FileEditorManager/getInstance project) file true))))))
144 |
145 | (.addDocumentListener (.getDocument input)
146 | (proxy [DocumentListener] []
147 | (insertUpdate [_] (filter-list alarm input list model @files))
148 |
149 | (removeUpdate [_] (filter-list alarm input list model @files))
150 |
151 | (changedUpdate [_] nil)))
152 |
153 | (.showInCenterOf popup (.getContentComponent parent))))
154 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | #
4 | # Copyright © 2015-2021 the original authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | #
21 | # Gradle start up script for POSIX generated by Gradle.
22 | #
23 | # Important for running:
24 | #
25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
26 | # noncompliant, but you have some other compliant shell such as ksh or
27 | # bash, then to run this script, type that shell name before the whole
28 | # command line, like:
29 | #
30 | # ksh Gradle
31 | #
32 | # Busybox and similar reduced shells will NOT work, because this script
33 | # requires all of these POSIX shell features:
34 | # * functions;
35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»;
37 | # * compound commands having a testable exit status, especially «case»;
38 | # * various built-in commands including «command», «set», and «ulimit».
39 | #
40 | # Important for patching:
41 | #
42 | # (2) This script targets any POSIX shell, so it avoids extensions provided
43 | # by Bash, Ksh, etc; in particular arrays are avoided.
44 | #
45 | # The "traditional" practice of packing multiple parameters into a
46 | # space-separated string is a well documented source of bugs and security
47 | # problems, so this is (mostly) avoided, by progressively accumulating
48 | # options in "$@", and eventually passing that to Java.
49 | #
50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
52 | # see the in-line comments for details.
53 | #
54 | # There are tweaks for specific operating systems such as AIX, CygWin,
55 | # Darwin, MinGW, and NonStop.
56 | #
57 | # (3) This script is generated from the Groovy template
58 | # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
59 | # within the Gradle project.
60 | #
61 | # You can find Gradle at https://github.com/gradle/gradle/.
62 | #
63 | ##############################################################################
64 |
65 | # Attempt to set APP_HOME
66 |
67 | # Resolve links: $0 may be a link
68 | app_path=$0
69 |
70 | # Need this for daisy-chained symlinks.
71 | while
72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
73 | [ -h "$app_path" ]
74 | do
75 | ls=$( ls -ld "$app_path" )
76 | link=${ls#*' -> '}
77 | case $link in #(
78 | /*) app_path=$link ;; #(
79 | *) app_path=$APP_HOME$link ;;
80 | esac
81 | done
82 |
83 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
84 |
85 | APP_NAME="Gradle"
86 | APP_BASE_NAME=${0##*/}
87 |
88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
90 |
91 | # Use the maximum available, or set MAX_FD != -1 to use that value.
92 | MAX_FD=maximum
93 |
94 | warn () {
95 | echo "$*"
96 | } >&2
97 |
98 | die () {
99 | echo
100 | echo "$*"
101 | echo
102 | exit 1
103 | } >&2
104 |
105 | # OS specific support (must be 'true' or 'false').
106 | cygwin=false
107 | msys=false
108 | darwin=false
109 | nonstop=false
110 | case "$( uname )" in #(
111 | CYGWIN* ) cygwin=true ;; #(
112 | Darwin* ) darwin=true ;; #(
113 | MSYS* | MINGW* ) msys=true ;; #(
114 | NONSTOP* ) nonstop=true ;;
115 | esac
116 |
117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
118 |
119 |
120 | # Determine the Java command to use to start the JVM.
121 | if [ -n "$JAVA_HOME" ] ; then
122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
123 | # IBM's JDK on AIX uses strange locations for the executables
124 | JAVACMD=$JAVA_HOME/jre/sh/java
125 | else
126 | JAVACMD=$JAVA_HOME/bin/java
127 | fi
128 | if [ ! -x "$JAVACMD" ] ; then
129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
130 |
131 | Please set the JAVA_HOME variable in your environment to match the
132 | location of your Java installation."
133 | fi
134 | else
135 | JAVACMD=java
136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
137 |
138 | Please set the JAVA_HOME variable in your environment to match the
139 | location of your Java installation."
140 | fi
141 |
142 | # Increase the maximum file descriptors if we can.
143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
144 | case $MAX_FD in #(
145 | max*)
146 | MAX_FD=$( ulimit -H -n ) ||
147 | warn "Could not query maximum file descriptor limit"
148 | esac
149 | case $MAX_FD in #(
150 | '' | soft) :;; #(
151 | *)
152 | ulimit -n "$MAX_FD" ||
153 | warn "Could not set maximum file descriptor limit to $MAX_FD"
154 | esac
155 | fi
156 |
157 | # Collect all arguments for the java command, stacking in reverse order:
158 | # * args from the command line
159 | # * the main class name
160 | # * -classpath
161 | # * -D...appname settings
162 | # * --module-path (only if needed)
163 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
164 |
165 | # For Cygwin or MSYS, switch paths to Windows format before running java
166 | if "$cygwin" || "$msys" ; then
167 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
168 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
169 |
170 | JAVACMD=$( cygpath --unix "$JAVACMD" )
171 |
172 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
173 | for arg do
174 | if
175 | case $arg in #(
176 | -*) false ;; # don't mess with options #(
177 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
178 | [ -e "$t" ] ;; #(
179 | *) false ;;
180 | esac
181 | then
182 | arg=$( cygpath --path --ignore --mixed "$arg" )
183 | fi
184 | # Roll the args list around exactly as many times as the number of
185 | # args, so each arg winds up back in the position where it started, but
186 | # possibly modified.
187 | #
188 | # NB: a `for` loop captures its iteration list before it begins, so
189 | # changing the positional parameters here affects neither the number of
190 | # iterations, nor the values presented in `arg`.
191 | shift # remove old arg
192 | set -- "$@" "$arg" # push replacement arg
193 | done
194 | fi
195 |
196 | # Collect all arguments for the java command;
197 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
198 | # shell script including quotes and variable substitutions, so put them in
199 | # double quotes to make sure that they get re-expanded; and
200 | # * put everything else in single quotes, so that it's not re-expanded.
201 |
202 | set -- \
203 | "-Dorg.gradle.appname=$APP_BASE_NAME" \
204 | -classpath "$CLASSPATH" \
205 | org.gradle.wrapper.GradleWrapperMain \
206 | "$@"
207 |
208 | # Use "xargs" to parse quoted args.
209 | #
210 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed.
211 | #
212 | # In Bash we could simply go:
213 | #
214 | # readarray ARGS < <( xargs -n1 <<<"$var" ) &&
215 | # set -- "${ARGS[@]}" "$@"
216 | #
217 | # but POSIX shell has neither arrays nor command substitution, so instead we
218 | # post-process each arg (as a line of input to sed) to backslash-escape any
219 | # character that might be a shell metacharacter, then use eval to reverse
220 | # that process (while maintaining the separation between arguments), and wrap
221 | # the whole thing up as a single "set" statement.
222 | #
223 | # This will of course break if any of these variables contains a newline or
224 | # an unmatched quote.
225 | #
226 |
227 | eval "set -- $(
228 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
229 | xargs -n1 |
230 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
231 | tr '\n' ' '
232 | )" '"$@"'
233 |
234 | exec "$JAVACMD" "$@"
235 |
--------------------------------------------------------------------------------
/src/main/clojure/fominok/ideahelix/keymap.clj:
--------------------------------------------------------------------------------
1 | ;; This Source Code Form is subject to the terms of the Mozilla Public
2 | ;; License, v. 2.0. If a copy of the MPL was not distributed with this
3 | ;; file, You can obtain one at https://mozilla.org/MPL/2.0/.
4 |
5 | (ns fominok.ideahelix.keymap
6 | "Keymap definition utilities."
7 | (:require
8 | [clojure.spec.alpha :as s]
9 | [fominok.ideahelix.editor.jumplist :refer [jumplist-add]]
10 | [fominok.ideahelix.editor.selection :refer [scroll-to-primary-caret]]
11 | [fominok.ideahelix.editor.util :refer [deep-merge]])
12 | (:import
13 | (com.intellij.openapi.command
14 | CommandProcessor
15 | WriteCommandAction)
16 | (com.intellij.openapi.command.impl
17 | FinishMarkAction
18 | StartMarkAction)
19 | (com.intellij.openapi.editor.impl
20 | EditorImpl)
21 | (com.intellij.openapi.project
22 | Project)
23 | (java.awt.event
24 | KeyEvent)))
25 |
26 |
27 | ;; Key matcher.
28 | (s/def ::matcher-core
29 | (s/or :symbol symbol?
30 | :int int?
31 | :char char?))
32 |
33 |
34 | ;; Matcher with possible modifier key applied.
35 | (s/def ::matcher
36 | (s/or :matcher-core ::matcher-core
37 | :with-ctrl (s/cat
38 | :tag (partial = :ctrl)
39 | :matcher-core ::matcher-core)
40 | :with-alt (s/cat
41 | :tag (partial = :alt)
42 | :matcher-core ::matcher-core)
43 | :with-shift (s/cat
44 | :tag (partial = :shift)
45 | :matcher-core ::matcher-core)
46 | :or (s/cat
47 | :tag (partial = :or)
48 | :matchers (s/+ ::matcher))))
49 |
50 |
51 | ;; One of possible dependencies of the statement to expect.
52 | (s/def ::dep
53 | (s/or :char (partial = 'char) ; typed character
54 | :event (partial = 'event) ; keyboard event that triggered this handler
55 | :editor (partial = 'editor) ; editor in focus
56 | :state (partial = 'state) ; ideahelix->project state
57 | :document (partial = 'document) ; document instance running in the editor
58 | :caret (partial = 'caret) ; one caret, makes body applied to each one equally
59 | :project (partial = 'project))) ; wrap into "critical section" required on modifications)) ;; wraps body into a single undoable action
60 |
61 |
62 | ;; Statement to execute. If the statement is just a :pass keyword
63 | ;; it deserves a special treatment.
64 | (s/def ::statement
65 | (s/or :pass (partial = :pass)
66 | :statement any?))
67 |
68 |
69 | ;; Similarly to function definitions, body is made of arguments vector
70 | ;; and a statement to execute.
71 | (s/def ::body
72 | (s/cat
73 | :deps (s/spec (s/coll-of ::dep))
74 | :statement ::statement))
75 |
76 |
77 | ;; Key* to bodies to execute mapping, where input can be matched in several ways.
78 | ;; There are several bodies possible that will be executed sequentially with
79 | ;; their own dependencies each.
80 | (s/def ::mapping
81 | (s/cat :matcher ::matcher
82 | :doc (s/? string?)
83 | :extras (s/* (s/or :undoable (partial = :undoable)
84 | :keep-prefix (partial = :keep-prefix)
85 | :write (partial = :write)
86 | :scroll (partial = :scroll)
87 | :jumplist-add (partial = :jumplist-add)))
88 | :bodies (s/+ ::body)))
89 |
90 |
91 | (s/def ::mode-name
92 | (s/or :name keyword?
93 | :or (s/cat :tag (partial = :or)
94 | :mode-names (s/+ keyword?))))
95 |
96 |
97 | ;; Top-level sections of `defkeymap` are grouped by Helix mode with key
98 | ;; mappings defined inside for each
99 | (s/def ::mode
100 | (s/cat
101 | :mode ::mode-name
102 | :mappings (s/+ (s/spec ::mapping))))
103 |
104 |
105 | ;; The top level spec for `defkeymap` contents
106 | (s/def ::defkeymap
107 | (s/coll-of ::mode))
108 |
109 |
110 | (defn- process-single-matcher
111 | [matcher]
112 | "Matching process happens through a nested map attempting to do so with modifier,
113 | then matcher type and the actual matcher, and this function builds such a path of
114 | nested maps.
115 | It requires a bit of evaluation at macro expansion time to figure out the type of
116 | the symbol, because either it is a predicate and shall go as :fn type or evaluates
117 | into a value to compare key event with such as a key code."
118 | (let [modifier (get-in matcher [1 :tag])
119 | matcher-core (or (get-in matcher [1 :matcher-core 1])
120 | (get-in matcher [1 1]))
121 | evaluated-matcher (if (= '_ matcher-core)
122 | :any
123 | (eval matcher-core))
124 | matcher-type (cond
125 | (char? evaluated-matcher) :char
126 | (instance? Integer evaluated-matcher) :int
127 | (fn? evaluated-matcher) :fn)]
128 | (if (= evaluated-matcher :any)
129 | [:any]
130 | [modifier matcher-type evaluated-matcher])))
131 |
132 |
133 | (defn- process-matcher
134 | "process-single-matcher has the core logic, but first we handle (:or ...) construct"
135 | [matcher]
136 | (if (= (first matcher) :or)
137 | (let [matchers (get-in matcher [1 :matchers])]
138 | (map process-single-matcher matchers))
139 | [(process-single-matcher matcher)]))
140 |
141 |
142 | (defn- process-body
143 | "Taking a dependencies vector and a statement, this function wraps the statement
144 | with requested dependencies with symbols linked.
145 | For `caret` dependency the body will be executed for each caret."
146 | [project project-state editor event {:keys [deps statement]}]
147 | (let [deps-bindings-split (group-by #(#{:caret} (first %)) deps)
148 | deps-bindings-top
149 | (into [] (mapcat
150 | (fn [[kw sym]]
151 | [sym (case kw
152 | :event event
153 | :state project-state
154 | :project project
155 | :document `(.getDocument ~editor)
156 | :char `(.getKeyChar ~event)
157 | :editor editor)])
158 | (get deps-bindings-split nil)))
159 | caret-sym (get-in deps-bindings-split [:caret 0 1])
160 | gen-statement
161 | (cond-> (second statement)
162 | caret-sym
163 | ((fn [s]
164 | `(let [caret-model# (.getCaretModel ~editor)]
165 | (.runForEachCaret
166 | caret-model#
167 | (fn [~caret-sym] ~s))))))]
168 | `(let ~deps-bindings-top
169 | ~gen-statement)))
170 |
171 |
172 | (defn- process-bodies
173 | "Builds handler function made of sequentially executed statements with dependencies
174 | injected for each."
175 | [bodies extras doc]
176 | (let [project (gensym "project")
177 | project-state (gensym "project-state")
178 | editor (gensym "editor")
179 | event (gensym "event")
180 | bodies (map (partial process-body project project-state editor event) bodies)
181 | docstring (or (str "IHx: " doc) "IdeaHelix command")
182 | statement
183 | (cond-> `(deep-merge ~@bodies)
184 | (extras :scroll) ((fn [s]
185 | `(let [return# ~s]
186 | (scroll-to-primary-caret ~editor)
187 | return#)))
188 | (extras :undoable) ((fn [s]
189 | `(let [return# (volatile! nil)]
190 | (.. CommandProcessor getInstance
191 | (executeCommand
192 | ~project
193 | (fn []
194 | (let [start# (StartMarkAction/start ~editor ~project ~docstring)]
195 | (try
196 | (vreset! return# ~s)
197 | (finally
198 | (FinishMarkAction/finish ~project ~editor start#)))))
199 | ~docstring
200 | nil))
201 | @return#)))
202 | (extras :write) ((fn [s]
203 | `(let [return# (volatile! nil)]
204 | (WriteCommandAction/runWriteCommandAction
205 | ~project
206 | (fn [] (vreset! return# ~s)))
207 | @return#)))
208 | (not (extras :keep-prefix)) ((fn [s]
209 | `(let [return# ~s]
210 | (assoc (if (map? return#) return# ~project-state) :prefix nil))))
211 | (extras :jumplist-add) ((fn [s]
212 | `(let [jl-pre# (jumplist-add ~project ~project-state)
213 | return# ~s
214 | jl-after# (jumplist-add ~project jl-pre#)]
215 | (merge return# (select-keys jl-after# [:jumplist]))))))]
216 | `(fn [^Project ~project ~project-state ^EditorImpl ~editor ^KeyEvent ~event]
217 | (merge ~project-state ~statement))))
218 |
219 |
220 | (defn- process-mappings
221 | "Inserts into mode mapping a handler function (sexp)
222 | under a matcher path ([mode literal-type/fn literal-value/predicate])."
223 | [mappings]
224 | (reduce
225 | (fn [acc m]
226 | (let [bodies (process-bodies (:bodies m) (into #{} (map first) (:extras m)) (:doc m))
227 | match-paths (process-matcher (:matcher m))]
228 | (reduce (fn [a p] (assoc-in a p bodies)) acc match-paths)))
229 | {}
230 | mappings))
231 |
232 |
233 | (defn- flatten-modes
234 | [acc {:keys [mode mappings]}]
235 | (case (first mode)
236 | :name (update acc (second mode) (fnil concat []) mappings)
237 | :or (reduce
238 | (fn [inner-acc mode-name]
239 | (update inner-acc mode-name (fnil concat []) mappings)) acc (get-in mode [1 :mode-names]))))
240 |
241 |
242 | (defmacro defkeymap
243 | "Define a keymap function that matches a key event by set of rules and handles it."
244 | [ident & mappings]
245 | (let [rules
246 | (as-> (s/conform ::defkeymap mappings) $
247 | (reduce flatten-modes {} $)
248 | (update-vals $ process-mappings))]
249 | `(defn ~ident [project# project-state# ^EditorImpl editor# ^KeyEvent event#]
250 | (let [modifier# (or (when (.isControlDown event#) :ctrl)
251 | (when (.isAltDown event#) :alt)
252 | (when (.isShiftDown event#) :shift))
253 | mode# (:mode project-state#)
254 | any-mode-matchers# (get-in ~rules [:any modifier#])
255 | cur-mode-matchers# (get-in ~rules [mode# modifier#])
256 | find-matcher#
257 | (fn [in-mode#]
258 | (or (get-in in-mode# [:int (.getKeyCode event#)])
259 | (get-in in-mode# [:char (.getKeyChar event#)])
260 | (some
261 | (fn [[pred# f#]] (when (pred# (.getKeyChar event#)) f#))
262 | (get in-mode# :fn))))
263 | handler-opt#
264 | (or (find-matcher# any-mode-matchers#)
265 | (find-matcher# cur-mode-matchers#)
266 | (get-in ~rules [mode# :any]))]
267 | (if-let [handler# handler-opt#]
268 | (handler# project# project-state# editor# event#))))))
269 |
--------------------------------------------------------------------------------
/ideahelix_light.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
125 |
--------------------------------------------------------------------------------
/ideahelix_dark.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
125 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Mozilla Public License Version 2.0
2 | ==================================
3 |
4 | 1. Definitions
5 | --------------
6 |
7 | 1.1. "Contributor"
8 | means each individual or legal entity that creates, contributes to
9 | the creation of, or owns Covered Software.
10 |
11 | 1.2. "Contributor Version"
12 | means the combination of the Contributions of others (if any) used
13 | by a Contributor and that particular Contributor's Contribution.
14 |
15 | 1.3. "Contribution"
16 | means Covered Software of a particular Contributor.
17 |
18 | 1.4. "Covered Software"
19 | means Source Code Form to which the initial Contributor has attached
20 | the notice in Exhibit A, the Executable Form of such Source Code
21 | Form, and Modifications of such Source Code Form, in each case
22 | including portions thereof.
23 |
24 | 1.5. "Incompatible With Secondary Licenses"
25 | means
26 |
27 | (a) that the initial Contributor has attached the notice described
28 | in Exhibit B to the Covered Software; or
29 |
30 | (b) that the Covered Software was made available under the terms of
31 | version 1.1 or earlier of the License, but not also under the
32 | terms of a Secondary License.
33 |
34 | 1.6. "Executable Form"
35 | means any form of the work other than Source Code Form.
36 |
37 | 1.7. "Larger Work"
38 | means a work that combines Covered Software with other material, in
39 | a separate file or files, that is not Covered Software.
40 |
41 | 1.8. "License"
42 | means this document.
43 |
44 | 1.9. "Licensable"
45 | means having the right to grant, to the maximum extent possible,
46 | whether at the time of the initial grant or subsequently, any and
47 | all of the rights conveyed by this License.
48 |
49 | 1.10. "Modifications"
50 | means any of the following:
51 |
52 | (a) any file in Source Code Form that results from an addition to,
53 | deletion from, or modification of the contents of Covered
54 | Software; or
55 |
56 | (b) any new file in Source Code Form that contains any Covered
57 | Software.
58 |
59 | 1.11. "Patent Claims" of a Contributor
60 | means any patent claim(s), including without limitation, method,
61 | process, and apparatus claims, in any patent Licensable by such
62 | Contributor that would be infringed, but for the grant of the
63 | License, by the making, using, selling, offering for sale, having
64 | made, import, or transfer of either its Contributions or its
65 | Contributor Version.
66 |
67 | 1.12. "Secondary License"
68 | means either the GNU General Public License, Version 2.0, the GNU
69 | Lesser General Public License, Version 2.1, the GNU Affero General
70 | Public License, Version 3.0, or any later versions of those
71 | licenses.
72 |
73 | 1.13. "Source Code Form"
74 | means the form of the work preferred for making modifications.
75 |
76 | 1.14. "You" (or "Your")
77 | means an individual or a legal entity exercising rights under this
78 | License. For legal entities, "You" includes any entity that
79 | controls, is controlled by, or is under common control with You. For
80 | purposes of this definition, "control" means (a) the power, direct
81 | or indirect, to cause the direction or management of such entity,
82 | whether by contract or otherwise, or (b) ownership of more than
83 | fifty percent (50%) of the outstanding shares or beneficial
84 | ownership of such entity.
85 |
86 | 2. License Grants and Conditions
87 | --------------------------------
88 |
89 | 2.1. Grants
90 |
91 | Each Contributor hereby grants You a world-wide, royalty-free,
92 | non-exclusive license:
93 |
94 | (a) under intellectual property rights (other than patent or trademark)
95 | Licensable by such Contributor to use, reproduce, make available,
96 | modify, display, perform, distribute, and otherwise exploit its
97 | Contributions, either on an unmodified basis, with Modifications, or
98 | as part of a Larger Work; and
99 |
100 | (b) under Patent Claims of such Contributor to make, use, sell, offer
101 | for sale, have made, import, and otherwise transfer either its
102 | Contributions or its Contributor Version.
103 |
104 | 2.2. Effective Date
105 |
106 | The licenses granted in Section 2.1 with respect to any Contribution
107 | become effective for each Contribution on the date the Contributor first
108 | distributes such Contribution.
109 |
110 | 2.3. Limitations on Grant Scope
111 |
112 | The licenses granted in this Section 2 are the only rights granted under
113 | this License. No additional rights or licenses will be implied from the
114 | distribution or licensing of Covered Software under this License.
115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a
116 | Contributor:
117 |
118 | (a) for any code that a Contributor has removed from Covered Software;
119 | or
120 |
121 | (b) for infringements caused by: (i) Your and any other third party's
122 | modifications of Covered Software, or (ii) the combination of its
123 | Contributions with other software (except as part of its Contributor
124 | Version); or
125 |
126 | (c) under Patent Claims infringed by Covered Software in the absence of
127 | its Contributions.
128 |
129 | This License does not grant any rights in the trademarks, service marks,
130 | or logos of any Contributor (except as may be necessary to comply with
131 | the notice requirements in Section 3.4).
132 |
133 | 2.4. Subsequent Licenses
134 |
135 | No Contributor makes additional grants as a result of Your choice to
136 | distribute the Covered Software under a subsequent version of this
137 | License (see Section 10.2) or under the terms of a Secondary License (if
138 | permitted under the terms of Section 3.3).
139 |
140 | 2.5. Representation
141 |
142 | Each Contributor represents that the Contributor believes its
143 | Contributions are its original creation(s) or it has sufficient rights
144 | to grant the rights to its Contributions conveyed by this License.
145 |
146 | 2.6. Fair Use
147 |
148 | This License is not intended to limit any rights You have under
149 | applicable copyright doctrines of fair use, fair dealing, or other
150 | equivalents.
151 |
152 | 2.7. Conditions
153 |
154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
155 | in Section 2.1.
156 |
157 | 3. Responsibilities
158 | -------------------
159 |
160 | 3.1. Distribution of Source Form
161 |
162 | All distribution of Covered Software in Source Code Form, including any
163 | Modifications that You create or to which You contribute, must be under
164 | the terms of this License. You must inform recipients that the Source
165 | Code Form of the Covered Software is governed by the terms of this
166 | License, and how they can obtain a copy of this License. You may not
167 | attempt to alter or restrict the recipients' rights in the Source Code
168 | Form.
169 |
170 | 3.2. Distribution of Executable Form
171 |
172 | If You distribute Covered Software in Executable Form then:
173 |
174 | (a) such Covered Software must also be made available in Source Code
175 | Form, as described in Section 3.1, and You must inform recipients of
176 | the Executable Form how they can obtain a copy of such Source Code
177 | Form by reasonable means in a timely manner, at a charge no more
178 | than the cost of distribution to the recipient; and
179 |
180 | (b) You may distribute such Executable Form under the terms of this
181 | License, or sublicense it under different terms, provided that the
182 | license for the Executable Form does not attempt to limit or alter
183 | the recipients' rights in the Source Code Form under this License.
184 |
185 | 3.3. Distribution of a Larger Work
186 |
187 | You may create and distribute a Larger Work under terms of Your choice,
188 | provided that You also comply with the requirements of this License for
189 | the Covered Software. If the Larger Work is a combination of Covered
190 | Software with a work governed by one or more Secondary Licenses, and the
191 | Covered Software is not Incompatible With Secondary Licenses, this
192 | License permits You to additionally distribute such Covered Software
193 | under the terms of such Secondary License(s), so that the recipient of
194 | the Larger Work may, at their option, further distribute the Covered
195 | Software under the terms of either this License or such Secondary
196 | License(s).
197 |
198 | 3.4. Notices
199 |
200 | You may not remove or alter the substance of any license notices
201 | (including copyright notices, patent notices, disclaimers of warranty,
202 | or limitations of liability) contained within the Source Code Form of
203 | the Covered Software, except that You may alter any license notices to
204 | the extent required to remedy known factual inaccuracies.
205 |
206 | 3.5. Application of Additional Terms
207 |
208 | You may choose to offer, and to charge a fee for, warranty, support,
209 | indemnity or liability obligations to one or more recipients of Covered
210 | Software. However, You may do so only on Your own behalf, and not on
211 | behalf of any Contributor. You must make it absolutely clear that any
212 | such warranty, support, indemnity, or liability obligation is offered by
213 | You alone, and You hereby agree to indemnify every Contributor for any
214 | liability incurred by such Contributor as a result of warranty, support,
215 | indemnity or liability terms You offer. You may include additional
216 | disclaimers of warranty and limitations of liability specific to any
217 | jurisdiction.
218 |
219 | 4. Inability to Comply Due to Statute or Regulation
220 | ---------------------------------------------------
221 |
222 | If it is impossible for You to comply with any of the terms of this
223 | License with respect to some or all of the Covered Software due to
224 | statute, judicial order, or regulation then You must: (a) comply with
225 | the terms of this License to the maximum extent possible; and (b)
226 | describe the limitations and the code they affect. Such description must
227 | be placed in a text file included with all distributions of the Covered
228 | Software under this License. Except to the extent prohibited by statute
229 | or regulation, such description must be sufficiently detailed for a
230 | recipient of ordinary skill to be able to understand it.
231 |
232 | 5. Termination
233 | --------------
234 |
235 | 5.1. The rights granted under this License will terminate automatically
236 | if You fail to comply with any of its terms. However, if You become
237 | compliant, then the rights granted under this License from a particular
238 | Contributor are reinstated (a) provisionally, unless and until such
239 | Contributor explicitly and finally terminates Your grants, and (b) on an
240 | ongoing basis, if such Contributor fails to notify You of the
241 | non-compliance by some reasonable means prior to 60 days after You have
242 | come back into compliance. Moreover, Your grants from a particular
243 | Contributor are reinstated on an ongoing basis if such Contributor
244 | notifies You of the non-compliance by some reasonable means, this is the
245 | first time You have received notice of non-compliance with this License
246 | from such Contributor, and You become compliant prior to 30 days after
247 | Your receipt of the notice.
248 |
249 | 5.2. If You initiate litigation against any entity by asserting a patent
250 | infringement claim (excluding declaratory judgment actions,
251 | counter-claims, and cross-claims) alleging that a Contributor Version
252 | directly or indirectly infringes any patent, then the rights granted to
253 | You by any and all Contributors for the Covered Software under Section
254 | 2.1 of this License shall terminate.
255 |
256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all
257 | end user license agreements (excluding distributors and resellers) which
258 | have been validly granted by You or Your distributors under this License
259 | prior to termination shall survive termination.
260 |
261 | ************************************************************************
262 | * *
263 | * 6. Disclaimer of Warranty *
264 | * ------------------------- *
265 | * *
266 | * Covered Software is provided under this License on an "as is" *
267 | * basis, without warranty of any kind, either expressed, implied, or *
268 | * statutory, including, without limitation, warranties that the *
269 | * Covered Software is free of defects, merchantable, fit for a *
270 | * particular purpose or non-infringing. The entire risk as to the *
271 | * quality and performance of the Covered Software is with You. *
272 | * Should any Covered Software prove defective in any respect, You *
273 | * (not any Contributor) assume the cost of any necessary servicing, *
274 | * repair, or correction. This disclaimer of warranty constitutes an *
275 | * essential part of this License. No use of any Covered Software is *
276 | * authorized under this License except under this disclaimer. *
277 | * *
278 | ************************************************************************
279 |
280 | ************************************************************************
281 | * *
282 | * 7. Limitation of Liability *
283 | * -------------------------- *
284 | * *
285 | * Under no circumstances and under no legal theory, whether tort *
286 | * (including negligence), contract, or otherwise, shall any *
287 | * Contributor, or anyone who distributes Covered Software as *
288 | * permitted above, be liable to You for any direct, indirect, *
289 | * special, incidental, or consequential damages of any character *
290 | * including, without limitation, damages for lost profits, loss of *
291 | * goodwill, work stoppage, computer failure or malfunction, or any *
292 | * and all other commercial damages or losses, even if such party *
293 | * shall have been informed of the possibility of such damages. This *
294 | * limitation of liability shall not apply to liability for death or *
295 | * personal injury resulting from such party's negligence to the *
296 | * extent applicable law prohibits such limitation. Some *
297 | * jurisdictions do not allow the exclusion or limitation of *
298 | * incidental or consequential damages, so this exclusion and *
299 | * limitation may not apply to You. *
300 | * *
301 | ************************************************************************
302 |
303 | 8. Litigation
304 | -------------
305 |
306 | Any litigation relating to this License may be brought only in the
307 | courts of a jurisdiction where the defendant maintains its principal
308 | place of business and such litigation shall be governed by laws of that
309 | jurisdiction, without reference to its conflict-of-law provisions.
310 | Nothing in this Section shall prevent a party's ability to bring
311 | cross-claims or counter-claims.
312 |
313 | 9. Miscellaneous
314 | ----------------
315 |
316 | This License represents the complete agreement concerning the subject
317 | matter hereof. If any provision of this License is held to be
318 | unenforceable, such provision shall be reformed only to the extent
319 | necessary to make it enforceable. Any law or regulation which provides
320 | that the language of a contract shall be construed against the drafter
321 | shall not be used to construe this License against a Contributor.
322 |
323 | 10. Versions of the License
324 | ---------------------------
325 |
326 | 10.1. New Versions
327 |
328 | Mozilla Foundation is the license steward. Except as provided in Section
329 | 10.3, no one other than the license steward has the right to modify or
330 | publish new versions of this License. Each version will be given a
331 | distinguishing version number.
332 |
333 | 10.2. Effect of New Versions
334 |
335 | You may distribute the Covered Software under the terms of the version
336 | of the License under which You originally received the Covered Software,
337 | or under the terms of any subsequent version published by the license
338 | steward.
339 |
340 | 10.3. Modified Versions
341 |
342 | If you create software not governed by this License, and you want to
343 | create a new license for such software, you may create and use a
344 | modified version of this License if you rename the license and remove
345 | any references to the name of the license steward (except to note that
346 | such modified license differs from this License).
347 |
348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary
349 | Licenses
350 |
351 | If You choose to distribute Source Code Form that is Incompatible With
352 | Secondary Licenses under the terms of this version of the License, the
353 | notice described in Exhibit B of this License must be attached.
354 |
355 | Exhibit A - Source Code Form License Notice
356 | -------------------------------------------
357 |
358 | This Source Code Form is subject to the terms of the Mozilla Public
359 | License, v. 2.0. If a copy of the MPL was not distributed with this
360 | file, You can obtain one at https://mozilla.org/MPL/2.0/.
361 |
362 | If it is not possible or desirable to put the notice in a particular
363 | file, then You may include the notice in a location (such as a LICENSE
364 | file in a relevant directory) where a recipient would be likely to look
365 | for such a notice.
366 |
367 | You may add additional accurate notices of copyright ownership.
368 |
369 | Exhibit B - "Incompatible With Secondary Licenses" Notice
370 | ---------------------------------------------------------
371 |
372 | This Source Code Form is "Incompatible With Secondary Licenses", as
373 | defined by the Mozilla Public License, v. 2.0.
374 |
--------------------------------------------------------------------------------
/src/main/clojure/fominok/ideahelix/editor/selection.clj:
--------------------------------------------------------------------------------
1 | ;; This Source Code Form is subject to the terms of the Mozilla Public
2 | ;; License, v. 2.0. If a copy of the MPL was not distributed with this
3 | ;; file, You can obtain one at https://mozilla.org/MPL/2.0/.
4 |
5 | (ns fominok.ideahelix.editor.selection
6 | (:require
7 | [fominok.ideahelix.editor.util :refer [printable-char?]])
8 | (:import
9 | (com.intellij.openapi.editor
10 | Document
11 | ScrollType
12 | VisualPosition)
13 | (com.intellij.openapi.editor.actions
14 | CaretStopPolicy
15 | EditorActionUtil)
16 | (com.intellij.openapi.editor.impl
17 | CaretImpl
18 | DocumentImpl)
19 | (com.intellij.openapi.project
20 | Project)
21 | (com.intellij.openapi.ui
22 | Messages)))
23 |
24 |
25 | ;; Instead of counting positions between characters this wrapper
26 | ;; speaks in character indices, at least because when selection is getting
27 | ;; reversed on caret movement the pivot is a character in Helix rather than
28 | ;; a position between characters, meaning it will be kept selected unlike in
29 | ;; Idea
30 | (defrecord IhxSelection
31 | [^CaretImpl caret anchor offset in-append])
32 |
33 |
34 | (defn ihx-move-forward
35 | [selection n]
36 | (update selection :offset + n))
37 |
38 |
39 | (defn ihx-move-backward
40 | [selection n]
41 | (update selection :offset - n))
42 |
43 |
44 | (defn ihx-make-backward
45 | [{:keys [anchor offset] :as selection}]
46 | (if (> offset anchor)
47 | (assoc selection :anchor offset :offset anchor)
48 | selection))
49 |
50 |
51 | (defn ihx-make-forward
52 | [{:keys [anchor offset] :as selection}]
53 | (if (< offset anchor)
54 | (assoc selection :anchor offset :offset anchor)
55 | selection))
56 |
57 |
58 | (defn ihx-nudge
59 | [selection n]
60 | (-> selection
61 | (update :anchor + n)
62 | (update :offset + n)))
63 |
64 |
65 | (defn ihx-offset
66 | [selection offset]
67 | (assoc selection :offset offset))
68 |
69 |
70 | (defn ihx-selection
71 | [^DocumentImpl document ^CaretImpl caret & {:keys [insert-mode] :or {insert-mode false}}]
72 | (let [start (.getSelectionStart caret)
73 | end (.getSelectionEnd caret)
74 | text-length (.getTextLength document)
75 | original-length (- end start)
76 | offset' (.getOffset caret)
77 | [in-append offset] (if (and insert-mode (= offset' end))
78 | [true (dec offset')]
79 | [false offset'])
80 | is-forward (or (< original-length 2) (not= start offset))
81 | is-broken (or (and (> text-length 0) (= original-length 0))
82 | (and (not= offset start)
83 | (not= offset (dec end))))
84 | anchor
85 | (cond
86 | is-broken offset
87 | is-forward start
88 | :else (max 0 (dec end)))]
89 | (->IhxSelection caret anchor offset in-append)))
90 |
91 |
92 | ;; This modifies the caret
93 | (defn ihx-apply-selection!
94 | [{:keys [anchor offset caret in-append]} document]
95 | (when (and caret (.isValid caret))
96 | (let [[start end] (sort [anchor offset])
97 | text-length (.getTextLength document)
98 | adj #(max 0 (min % (dec text-length)))
99 | adjusted-offset (adj (cond-> offset
100 | in-append inc))
101 | adjusted-start (adj start)]
102 | (.moveToOffset caret adjusted-offset)
103 | (.setSelection caret adjusted-start (max 0 (min (inc end) text-length))))))
104 |
105 |
106 | (defn ihx-apply-selection-preserving
107 | [{:keys [anchor offset caret]} document]
108 | (let [[start end] (sort [anchor offset])
109 | adjusted-start (max 0 start)
110 | adjusted-end (min (.getTextLength document) (inc end))]
111 | (.setSelection caret adjusted-start adjusted-end)))
112 |
113 |
114 | (defn ihx-shrink-selection
115 | [selection]
116 | (assoc selection :anchor (:offset selection)))
117 |
118 |
119 | (defn flip-selection
120 | [{:keys [offset anchor] :as selection}]
121 | (if (> offset anchor)
122 | (ihx-make-backward selection)
123 | (ihx-make-forward selection)))
124 |
125 |
126 | (defn keep-primary-selection
127 | [editor]
128 | (.. editor getCaretModel removeSecondaryCarets))
129 |
130 |
131 | (defn reversed?
132 | [caret]
133 | (let [selection-start (.getSelectionStart caret)
134 | selection-end (.getSelectionEnd caret)
135 | offset (.getOffset caret)]
136 | (and
137 | (= offset selection-start)
138 | (> (- selection-end selection-start) 1))))
139 |
140 |
141 | (defn ihx-move-line-start
142 | [{:keys [offset] :as selection} editor document]
143 | (let [line-start-offset
144 | (.getLineStartOffset document (.line (.offsetToLogicalPosition editor offset)))]
145 | (assoc selection :offset line-start-offset)))
146 |
147 |
148 | (defn ihx-move-line-end
149 | [{:keys [offset] :as selection} editor document]
150 | (let [line-end-offset
151 | (.getLineEndOffset document (.line (.offsetToLogicalPosition editor offset)))]
152 | (assoc selection :offset line-end-offset)))
153 |
154 |
155 | (defn ihx-move-relative!
156 | [{:keys [caret] :as selection} & {:keys [cols lines] :or {cols 0 lines 0}}]
157 | (.moveCaretRelatively caret cols lines false false)
158 | (assoc selection :offset (.getOffset caret)))
159 |
160 |
161 | (defn ihx-select-lines
162 | [{:keys [anchor offset] :as selection} editor document & {:keys [extend] :or {extend false}}]
163 | (let [new-selection
164 | (-> selection
165 | ihx-make-backward
166 | (ihx-move-line-start editor document)
167 | ihx-make-forward
168 | (ihx-move-line-end editor document))]
169 | (if (and extend (= (sort [anchor offset])
170 | (sort [(:anchor new-selection) (:offset new-selection)])))
171 | (-> new-selection
172 | (ihx-move-relative! :lines 1)
173 | (ihx-move-line-end editor document))
174 | new-selection)))
175 |
176 |
177 | (defn- line-length
178 | [document n]
179 | (let [start-offset (.getLineStartOffset document n)
180 | end-offset (.getLineEndOffset document n)]
181 | (- end-offset start-offset)))
182 |
183 |
184 | (defn- scan-next-selection-placement
185 | [document height start end lines-count]
186 | (let [start-column (.column start)
187 | end-column (.column end)]
188 | (loop [line (+ height 1 (.line start))]
189 | (let [line-end (+ line height)]
190 | (when-not (>= line-end lines-count)
191 | (let [start-line-length (line-length document line)
192 | end-line-length (line-length document line-end)]
193 | (if (and (<= start-column start-line-length)
194 | (<= end-column end-line-length))
195 | [line line-end]
196 | (recur (inc line)))))))))
197 |
198 |
199 | (defn add-selection-below
200 | [editor caret]
201 | (let [model (.getCaretModel editor)
202 | document (.getDocument editor)
203 | selection-start (.offsetToLogicalPosition editor (.getSelectionStart caret))
204 | selection-end (.offsetToLogicalPosition editor (.getSelectionEnd caret))
205 | caret-col (.column (.offsetToLogicalPosition editor (.getOffset caret)))
206 | height (- (.line selection-end)
207 | (.line selection-start))
208 | line-count (.. editor getDocument getLineCount)
209 | reversed (reversed? caret)]
210 | (when-let [[next-line-start next-line-end]
211 | (scan-next-selection-placement document height selection-start selection-end line-count)]
212 | (some-> (.addCaret model (VisualPosition.
213 | (if reversed
214 | next-line-start
215 | next-line-end) caret-col))
216 | (.setSelection (+ (.getLineStartOffset document next-line-start)
217 | (.column selection-start))
218 | (+ (.getLineStartOffset document next-line-end)
219 | (.column selection-end)))))))
220 |
221 |
222 | (defn select-buffer
223 | [editor document]
224 | (let [caret (.. editor getCaretModel getPrimaryCaret)
225 | length (.getTextLength document)]
226 | (-> (ihx-selection document caret)
227 | (ihx-offset 0)
228 | ihx-make-forward
229 | (ihx-offset length)
230 | (ihx-apply-selection! document))))
231 |
232 |
233 | (defn regex-matches-with-positions
234 | [pattern text]
235 | (let [matcher (re-matcher pattern text)]
236 | (loop [results []]
237 | (if (.find matcher)
238 | (recur (conj results {:start (.start matcher)
239 | :end (.end matcher)}))
240 | results))))
241 |
242 |
243 | (defn select-in-selections
244 | [^Project project editor document]
245 | (let [model (.getCaretModel editor)
246 | primary (.getPrimaryCaret model)
247 | input (Messages/showInputDialog
248 | project
249 | "select:"
250 | "Select in selections"
251 | (Messages/getQuestionIcon))
252 | pattern (when (seq input) (re-pattern input))
253 | matches
254 | (and pattern
255 | (->> (.getAllCarets model)
256 | (map (fn [caret] [(.getSelectionStart caret) (.getText document (.getSelectionRange caret))]))
257 | (map (fn [[offset text]]
258 | (map #(update-vals % (partial + offset))
259 | (regex-matches-with-positions pattern text))))
260 | flatten))]
261 | (when-let [{:keys [start end]} (first matches)]
262 | (.removeSecondaryCarets model)
263 | (-> (->IhxSelection primary start (dec end) false)
264 | (ihx-apply-selection! document)))
265 | (doseq [{:keys [start end]} (rest matches)]
266 | (when-let [caret (.addCaret model (.offsetToVisualPosition editor (max 0 (dec end))))]
267 | (-> (->IhxSelection caret start (dec end) false)
268 | (ihx-apply-selection! document))))))
269 |
270 |
271 | (defn scroll-to-primary-caret
272 | [editor]
273 | (.. editor getScrollingModel (scrollToCaret ScrollType/RELATIVE)))
274 |
275 |
276 | (defn find-next-occurrence
277 | [^CharSequence text {:keys [pos neg]}]
278 | (first (keep-indexed #(when (or
279 | (contains? pos %2)
280 | (not (or (nil? neg) (contains? neg %2))))
281 | %1) text)))
282 |
283 |
284 | (defn find-char
285 | [state editor ^Document document char & {:keys [include]
286 | :or {include false}}]
287 | (when
288 | (or (Character/isLetterOrDigit char) ((into #{} "!@#$%^&*()_+-={}[]|;:<>.,?~`") char))
289 | (doseq [caret (.. editor getCaretModel getAllCarets)]
290 | (let [text (.getCharsSequence document)
291 | len (.length text)
292 | expand (= (:previous-mode state) :select)
293 | sub (.subSequence text (+ 2 (.getOffset caret)) len)]
294 | (when-let [delta (find-next-occurrence sub {:pos #{char}})]
295 | (cond-> (ihx-selection document caret)
296 | (not expand) (ihx-shrink-selection)
297 | true (ihx-move-forward (inc delta))
298 | include (ihx-move-forward 1)
299 | true (ihx-apply-selection! document)))))
300 | (assoc state :mode (:previous-mode state))))
301 |
302 |
303 | ;; This modifies the caret
304 | (defn ihx-word-forward-extending!
305 | [{:keys [caret] :as selection} editor]
306 | (.moveCaretRelatively caret 1 0 false false)
307 | (EditorActionUtil/moveToNextCaretStop editor CaretStopPolicy/WORD_START false true)
308 | (let [new-offset (max 0 (dec (.getOffset caret)))]
309 | (assoc selection :offset new-offset)))
310 |
311 |
312 | (defn ihx-word-forward!
313 | [{:keys [caret offset] :as selection} editor]
314 | (EditorActionUtil/moveToNextCaretStop editor CaretStopPolicy/WORD_START false true)
315 | (let [new-offset (.getOffset caret)]
316 | (if (= new-offset (inc offset))
317 | (do
318 | (EditorActionUtil/moveToNextCaretStop editor CaretStopPolicy/WORD_START false true)
319 | (assoc selection :offset (max 0 (dec (.getOffset caret))) :anchor new-offset))
320 | (assoc selection :offset (max 0 (dec new-offset)) :anchor offset))))
321 |
322 |
323 | (defn ihx-long-word-forward!
324 | [{:keys [_ offset] :as selection} document extending?]
325 | (let [text (.getCharsSequence document)
326 | blank-chars (set " \t\n\r")
327 | len (.length text)
328 | offset (cond-> offset
329 | ;; if we are at space right before a word, we shift the offset
330 | (and
331 | (contains? blank-chars (.charAt text offset))
332 | (not (contains? blank-chars (.charAt text (inc offset)))))
333 | inc
334 | ;; if we are at the end of the line, we skip to next line
335 | (= \newline (.charAt text (inc offset)))
336 | (+ 2))
337 | sub (.subSequence text offset len)
338 | end-offset (+ offset (or (find-next-occurrence sub {:pos blank-chars}) (.length sub)))
339 | sub (.subSequence text end-offset len)
340 | end-offset (+ end-offset (or (find-next-occurrence sub {:pos #{} :neg (set " \t")}) (.length sub)))
341 | end-offset (dec end-offset)]
342 | (if extending?
343 | (assoc selection :offset end-offset)
344 | (assoc selection :offset end-offset :anchor offset))))
345 |
346 |
347 | (defn ihx-word-end-extending!
348 | [{:keys [caret] :as selection} editor]
349 | (.moveCaretRelatively caret 1 0 false false)
350 | (EditorActionUtil/moveToNextCaretStop editor CaretStopPolicy/WORD_END false true)
351 | (let [new-offset (max 0 (dec (.getOffset caret)))]
352 | (assoc selection :offset new-offset)))
353 |
354 |
355 | (defn ihx-word-end!
356 | [{:keys [caret offset] :as selection} editor]
357 | (EditorActionUtil/moveToNextCaretStop editor CaretStopPolicy/WORD_END false true)
358 | (let [new-offset (.getOffset caret)]
359 | (if (= new-offset (inc offset))
360 | (do
361 | (EditorActionUtil/moveToNextCaretStop editor CaretStopPolicy/WORD_END false true)
362 | (assoc selection :offset (max 0 (dec (.getOffset caret))) :anchor new-offset))
363 | (assoc selection :offset (max 0 (dec new-offset)) :anchor offset))))
364 |
365 |
366 | (defn ihx-long-word-end!
367 | [{:keys [_ offset] :as selection} document extending?]
368 | (let [text (.getCharsSequence document)
369 | blank-chars (set " \t\n\r")
370 | len (.length text)
371 | offset (cond-> offset
372 | ;; if we are at space right before a word, we shift the offset
373 | (and
374 | (not (contains? blank-chars (.charAt text offset)))
375 | (contains? blank-chars (.charAt text (inc offset))))
376 | inc
377 | ;; if we are at the end of the line, we skip to next line
378 | (= \newline (.charAt text (inc offset)))
379 | (+ 2))
380 | sub (.subSequence text offset len)
381 | end-offset (+ offset (or (find-next-occurrence sub {:pos #{} :neg blank-chars}) (.length sub)))
382 | sub (.subSequence text end-offset len)
383 | end-offset (+ end-offset (or (find-next-occurrence sub {:pos blank-chars}) (.length sub)))
384 | end-offset (dec end-offset)]
385 | (if extending?
386 | (assoc selection :offset end-offset)
387 | (assoc selection :offset end-offset :anchor offset))))
388 |
389 |
390 | (defn ihx-word-backward-extending!
391 | [{:keys [caret] :as selection} editor]
392 | (EditorActionUtil/moveToPreviousCaretStop editor CaretStopPolicy/WORD_START false true)
393 | (let [new-offset (.getOffset caret)]
394 | (assoc selection :offset new-offset)))
395 |
396 |
397 | (defn ihx-word-backward!
398 | [{:keys [caret offset] :as selection} editor]
399 | (EditorActionUtil/moveToPreviousCaretStop editor CaretStopPolicy/WORD_START false true)
400 | (let [new-selection (assoc selection :offset (.getOffset caret) :anchor offset)]
401 | (EditorActionUtil/moveToNextCaretStop editor CaretStopPolicy/WORD_START false true)
402 | (if (= (.getOffset caret) offset)
403 | (assoc new-selection :anchor (max 0 (dec (.getOffset caret))))
404 | new-selection)))
405 |
406 |
407 | (defn ihx-long-word-backward!
408 | [{:keys [_ offset] :as selection} document extending?]
409 | (let [text (-> (.getCharsSequence document) (.subSequence 0 offset) StringBuilder. .reverse)
410 | blank-chars (set " \t\n\r")
411 | len (.length text)
412 | start (cond-> 1
413 | (and
414 | (not (contains? blank-chars (.charAt text 0)))
415 | (contains? blank-chars (.charAt text 1)))
416 | inc
417 | (= \newline (.charAt text 1))
418 | (+ 2))
419 | sub (.subSequence text start len)
420 | end-offset (+ start (or (find-next-occurrence sub {:pos #{\newline} :neg blank-chars}) (.length sub)))
421 | sub (.subSequence text end-offset len)
422 | end-offset (+ end-offset (or (find-next-occurrence sub {:pos blank-chars}) (.length sub)))
423 | end-offset (dec end-offset)]
424 | (if extending?
425 | (assoc selection :offset (- offset end-offset 1))
426 | (assoc selection :offset (- offset end-offset 1) :anchor (- offset start)))))
427 |
428 |
429 | (defn ihx-move-caret-line-n
430 | [editor document n]
431 | (let [line-n (dec (min n (.getLineCount document)))
432 | model (.getCaretModel editor)
433 | caret (.getPrimaryCaret model)
434 | offset (.getLineStartOffset document line-n)]
435 | (.removeSecondaryCarets model)
436 | (-> (ihx-selection document caret)
437 | (ihx-offset offset))))
438 |
439 |
440 | (defn ihx-move-file-end
441 | [editor document]
442 | (let [text-length (.getTextLength document)
443 | model (.getCaretModel editor)
444 | caret (.getPrimaryCaret model)]
445 | (.removeSecondaryCarets model)
446 | (-> (ihx-selection document caret)
447 | (ihx-offset text-length))))
448 |
449 |
450 | (defn dump-drop-selections!
451 | [editor document]
452 | (let [carets (.. editor getCaretModel getAllCarets)
453 | ihx-selections (doall (map (partial ihx-selection document) carets))]
454 | (doseq [caret carets
455 | :let [offset (.getOffset caret)]]
456 | (.setSelection caret offset offset))
457 | ihx-selections))
458 |
459 |
460 | (defn- restore-saved-selections
461 | [pre-selections insertion-kind document carets]
462 | (when-not (empty? pre-selections)
463 | (let [delta (- (.getOffset (first carets)) (:offset (first pre-selections))
464 | (if (= insertion-kind :append) 1 0))
465 | increasing-deltas (map (partial * delta) (range))]
466 | (doseq [[{:keys [caret anchor] :as selection} delta-anchor]
467 | (map vector pre-selections increasing-deltas)
468 | :let [new-offset (.getOffset caret)
469 | new-anchor (+ anchor delta-anchor)]]
470 | (case insertion-kind
471 | :append (if (>= new-offset new-anchor)
472 | (-> selection
473 | (assoc :anchor new-anchor)
474 | (assoc :offset (dec new-offset))
475 | (ihx-apply-selection! document))
476 | (-> (ihx-selection document caret)
477 | (ihx-move-backward 1)
478 | (ihx-apply-selection! document)))
479 | (-> selection
480 | (ihx-nudge (+ delta delta-anchor))
481 | (ihx-apply-selection! document)))))))
482 |
483 |
484 | (defn restore-selections
485 | [pre-selections insertion-kind editor document]
486 | (let [carets (.. editor getCaretModel getAllCarets)
487 | saved-carets (into #{} (map :caret pre-selections))
488 | free-carets (filter (complement saved-carets) carets)]
489 | (restore-saved-selections pre-selections insertion-kind document carets)
490 | (doseq [caret free-carets]
491 | (-> (ihx-selection document caret)
492 | (ihx-apply-selection! document)))))
493 |
494 |
495 | (def char-match
496 | {\( {:match \) :direction :open}
497 | \) {:match \( :direction :close}
498 | \[ {:match \] :direction :open}
499 | \] {:match \[ :direction :close}
500 | \{ {:match \} :direction :open}
501 | \} {:match \{ :direction :close}
502 | \< {:match \> :direction :open}
503 | \> {:match \< :direction :close}})
504 |
505 |
506 | (defn next-match
507 | [text offset opener target]
508 | (loop [to-find 1 text (.subSequence text offset (.length text)) acc-offset offset]
509 | (let [target-offset (find-next-occurrence text {:pos (set [opener target])})]
510 | (when target-offset
511 | (let [found (.charAt text target-offset)
512 | text (.subSequence text (inc target-offset) (.length text))
513 | acc-offset (+ acc-offset target-offset 1)]
514 | (if (= found target)
515 | (if (= to-find 1)
516 | (dec acc-offset)
517 | (recur (dec to-find) text acc-offset))
518 | (recur (inc to-find) text acc-offset)))))))
519 |
520 |
521 | (defn previous-match
522 | [text offset opener target]
523 | (let [len (.length text)
524 | idx (- len offset 1)
525 | text (-> (StringBuilder. text) .reverse)
526 | res (next-match text idx opener target)]
527 | (when res (- len res 1))))
528 |
529 |
530 | (defn get-open-close-chars
531 | [char]
532 | (let [match-info (get char-match char)]
533 | (cond (nil? match-info) {:open-char char :close-char char}
534 | (= (:direction match-info) :open) {:open-char char :close-char (:match match-info)}
535 | :else {:open-char (:match match-info) :close-char char})))
536 |
537 |
538 | (defn find-matches
539 | [{:keys [_ offset]} document char]
540 | (let [{:keys [open-char close-char]} (get-open-close-chars char)
541 | text (.getCharsSequence document)
542 | curr-char (.charAt (.getCharsSequence document) offset)]
543 | (if (and (not= open-char curr-char) (not= close-char curr-char))
544 | {:left (previous-match text offset close-char open-char)
545 | :right (next-match text offset open-char close-char)}
546 | (cond
547 | (= open-char close-char) nil
548 | (= open-char curr-char) {:left offset
549 | :right (next-match text (inc offset) open-char close-char)}
550 | (= close-char curr-char) {:left (previous-match text (dec offset) close-char open-char)
551 | :right offset}))))
552 |
553 |
554 | (defn ihx-select-inside
555 | [selection document char]
556 | (let [matches (find-matches selection document char)]
557 | (when (not (nil? matches))
558 | (assoc selection :offset (inc (:left matches)) :anchor (dec (:right matches))))))
559 |
560 |
561 | (defn ihx-select-around
562 | [selection document char]
563 | (let [matches (find-matches selection document char)]
564 | (when (not (nil? matches))
565 | (assoc selection :offset (:left matches) :anchor (:right matches)))))
566 |
567 |
568 | (defn ihx-surround-add
569 | [{:keys [offset anchor] :as selection} document char]
570 | (when (printable-char? char)
571 | (let [{:keys [open-char close-char]} (get-open-close-chars char)]
572 | (cond (> offset anchor)
573 | (do (.insertString document (inc offset) (str close-char))
574 | (.insertString document anchor (str open-char))
575 | (assoc selection :offset (+ 2 offset) :anchor anchor))
576 | :else
577 | (do (.insertString document (inc anchor) (str close-char))
578 | (.insertString document offset (str open-char))
579 | (assoc selection :offset offset :anchor (+ 2 anchor)))))))
580 |
581 |
582 | (defn ihx-surround-delete
583 | [{:keys [offset anchor] :as selection} document char]
584 | (when (printable-char? char)
585 | (let [text (.getCharsSequence document)
586 | {:keys [open-char close-char]} (get-open-close-chars char)
587 | curr-char (.charAt text offset)]
588 | (if (and (not= open-char curr-char) (not= close-char curr-char))
589 | (let [left (previous-match text offset close-char open-char)
590 | right (next-match text offset open-char close-char)
591 | anchor (if (> anchor left)
592 | (dec anchor)
593 | anchor)]
594 | (.deleteString document right (inc right))
595 | (.deleteString document left (inc left))
596 | (assoc selection :offset (dec offset) :anchor anchor))
597 | (cond
598 | (= open-char close-char) nil
599 | (= open-char curr-char) (let [left offset
600 | right (next-match text (inc offset) open-char close-char)
601 | anchor (if (> anchor offset) (dec anchor) anchor)]
602 | (.deleteString document right (inc right))
603 | (.deleteString document left (inc left))
604 | (assoc selection :offset offset :anchor anchor))
605 | (= close-char curr-char) (let [left (previous-match text (dec offset) close-char open-char)
606 | right offset
607 | anchor (dec anchor)]
608 | (.deleteString document right (inc right))
609 | (.deleteString document left (inc left))
610 | (assoc selection :offset (dec offset) :anchor anchor)))))))
611 |
612 |
613 | (defn ihx-goto-matching
614 | [{:keys [_ offset] :as selection} document]
615 | (let [text (.getCharsSequence document)
616 | opener (.charAt text offset)
617 | {:keys [match direction]} (get char-match opener)]
618 | (when (and match direction)
619 | (let [offset (case direction
620 | :open (next-match text (inc offset) opener match)
621 | :close (previous-match text (dec offset) opener match))]
622 | (assoc selection :offset offset)))))
623 |
624 |
625 | (defn- find-bracket-pair
626 | "Finds left and right bracket positions for the given offset, text, and chars."
627 | [text offset open-char close-char]
628 | (when-let [curr-char (and (< offset (count text)) (.charAt text offset))]
629 | (let [pair (if (and (not= open-char curr-char) (not= close-char curr-char))
630 | [(previous-match text offset close-char open-char)
631 | (next-match text offset open-char close-char)]
632 | (cond
633 | (= open-char curr-char) [offset (next-match text (inc offset) open-char close-char)]
634 | (= close-char curr-char) [(previous-match text (dec offset) close-char open-char) offset]))]
635 | (when (and (first pair) (second pair))
636 | pair))))
637 |
638 |
639 | (defn- add-caret-at
640 | "Adds a caret at the given offset (or uses primary if specified), sets single-char selection."
641 | [model editor offset use-primary?]
642 | (let [caret (if use-primary?
643 | (.getPrimaryCaret model)
644 | (.addCaret model (.offsetToVisualPosition editor offset) false))]
645 | (when caret
646 | (.setSelection caret offset (inc offset))
647 | (.moveToOffset caret offset))))
648 |
649 |
650 | (defn ihx-surround-find
651 | [state editor document char]
652 | (if-not (printable-char? char)
653 | state
654 | (let [model (.getCaretModel editor)
655 | original-carets (.getAllCarets model)
656 | original-selections (mapv #(ihx-selection document % :insert-mode false) original-carets)
657 | text (.getCharsSequence document)
658 | {:keys [open-char close-char]} (get-open-close-chars char)
659 | finder (fn [acc sel]
660 | (let [pair (find-bracket-pair text (:offset sel) open-char close-char)]
661 | (if pair (conj acc pair) acc)))
662 | pairs (reduce finder [] original-selections)]
663 | (if (empty? pairs)
664 | (assoc state :mode :normal)
665 | (do
666 | (.removeSecondaryCarets model)
667 | (loop [remaining-pairs pairs
668 | is-first? true]
669 | (when-let [[left right] (first remaining-pairs)]
670 | (add-caret-at model editor left is-first?)
671 | (add-caret-at model editor right false)
672 | (recur (rest remaining-pairs) false)))
673 | (assoc state :pre-match-selections original-selections
674 | :match-pairs pairs
675 | :mode :match-replace))))))
676 |
677 |
678 | (defn ihx-surround-replace
679 | [state editor document char]
680 | (if-not (printable-char? char)
681 | state
682 | (let [{:keys [open-char close-char]} (get-open-close-chars char)
683 | pairs (:match-pairs state)
684 | model (.getCaretModel editor)]
685 | (when (seq pairs)
686 | (doseq [[left right] pairs]
687 | (.replaceString document left (inc left) (str open-char))
688 | (.replaceString document right (inc right) (str close-char)))
689 | (.removeSecondaryCarets model)
690 | (doseq [[idx sel] (map-indexed vector (:pre-match-selections state))]
691 | (let [caret (if (zero? idx)
692 | (.getPrimaryCaret model)
693 | (.addCaret model (.offsetToVisualPosition editor (:offset sel)) false))]
694 | (when caret
695 | (ihx-apply-selection! (assoc sel :caret caret) document)))))
696 | (assoc state :mode :normal :pre-match-selections nil :match-pairs nil))))
697 |
--------------------------------------------------------------------------------
/src/main/clojure/fominok/ideahelix/editor.clj:
--------------------------------------------------------------------------------
1 | ;; This Source Code Form is subject to the terms of the Mozilla Public
2 | ;; License, v. 2.0. If a copy of the MPL was not distributed with this
3 | ;; file, You can obtain one at https://mozilla.org/MPL/2.0/.
4 |
5 | (ns fominok.ideahelix.editor
6 | (:require
7 | [fominok.ideahelix.editor.action :refer [actions]]
8 | [fominok.ideahelix.editor.jumplist :refer :all]
9 | [fominok.ideahelix.editor.modification :refer :all]
10 | [fominok.ideahelix.editor.registers :refer :all]
11 | [fominok.ideahelix.editor.selection :refer :all]
12 | [fominok.ideahelix.editor.ui :as ui]
13 | [fominok.ideahelix.editor.util :refer [get-editor-height]]
14 | [fominok.ideahelix.keymap :refer [defkeymap]]
15 | [fominok.ideahelix.search :refer :all])
16 | (:import
17 | (com.intellij.codeInsight.lookup
18 | LookupManager)
19 | (com.intellij.codeInsight.lookup.impl
20 | LookupImpl)
21 | (com.intellij.ide.actions.searcheverywhere
22 | SearchEverywhereManager)
23 | (com.intellij.openapi.actionSystem
24 | ActionPlaces
25 | AnActionEvent
26 | IdeActions)
27 | (com.intellij.openapi.command.impl
28 | StartMarkAction$AlreadyStartedException)
29 | (com.intellij.openapi.editor
30 | ScrollType)
31 | (com.intellij.openapi.editor.impl
32 | EditorImpl)
33 | (java.awt.event
34 | KeyEvent)))
35 |
36 |
37 | (defonce state-atom (atom {}))
38 |
39 |
40 | (def get-prefix
41 | (memoize
42 | (fn [state]
43 | (if-let [prefix-vec (get state :prefix)]
44 | (min 10000 (Integer/parseInt (apply str prefix-vec)))
45 | 1))))
46 |
47 |
48 | (defn quit-insert-mode
49 | [project state document]
50 | (doseq [[editor {:keys [mark-action pre-selections]}] (:per-editor state)]
51 | (restore-selections pre-selections (:insertion-kind state) editor document)
52 | (finish-undo project editor mark-action))
53 | (assoc state :mode :normal :prefix nil :per-editor nil))
54 |
55 |
56 | (defn- into-insert-mode
57 | [project
58 | state
59 | editor
60 | & {:keys [dump-selections insertion-kind]
61 | :or {dump-selections true insertion-kind :prepend}}]
62 | (let [pre-selections (when dump-selections (dump-drop-selections! editor (.getDocument editor)))]
63 | (-> state
64 | (assoc-in [:per-editor editor :mark-action] (start-undo project editor))
65 | (assoc-in [:per-editor editor :pre-selections] pre-selections)
66 | (assoc :mode :insert
67 | :debounce true
68 | :insertion-kind insertion-kind
69 | :prefix nil))))
70 |
71 |
72 | (defkeymap
73 | editor-handler
74 |
75 | (:any
76 | (KeyEvent/VK_ESCAPE
77 | "Back to normal mode"
78 | [state project editor document]
79 | (if (= :insert (:mode state))
80 | (quit-insert-mode project state document)
81 | (assoc state :mode :normal :prefix nil :pre-selections nil :insertion-kind nil))))
82 |
83 | (:find-char
84 | (_ [state editor document char event]
85 | (let [include (:find-char-include state)]
86 | (find-char state editor document char :include include))))
87 |
88 | ((:or :normal :select)
89 | (\space
90 | "Space menu"
91 | [state] (assoc state :mode :space))
92 | (\z
93 | "View menu"
94 | [state] (assoc state :mode :view :previous-mode (:mode state)))
95 | ((:or (:ctrl \w) (:ctrl \u0017))
96 | "Window menu"
97 | [state] (assoc state :mode :window))
98 | (\t
99 | "Find till char"
100 | [state] (assoc state :mode :find-char
101 | :find-char-include false
102 | :previous-mode (:mode state)))
103 | (\f
104 | "Find including char"
105 | [state] (assoc state :mode :find-char
106 | :find-char-include true
107 | :previous-mode (:mode state)))
108 | (\u
109 | "Undo"
110 | [editor] (actions editor IdeActions/ACTION_UNDO)
111 | [document caret] (-> (ihx-selection document caret)
112 | (ihx-apply-selection! document)))
113 | ((:shift \U)
114 | "Redo"
115 | [editor] (actions editor IdeActions/ACTION_REDO)
116 | [document caret] (-> (ihx-selection document caret)
117 | (ihx-apply-selection! document)))
118 | (\y
119 | "Yank"
120 | [state editor document]
121 | (let [registers (copy-to-register (:registers state) editor document)]
122 | (assoc state :registers registers)))
123 | (\o
124 | "New line below" :write :scroll
125 | [editor document caret]
126 | (do (-> (ihx-selection document caret)
127 | (ihx-move-line-end editor document)
128 | (ihx-apply-selection! document))
129 | (.setSelection caret (.getSelectionEnd caret) (.getSelectionEnd caret)))
130 | [project editor state]
131 | (let [new-state (into-insert-mode project state editor :dump-selections false)]
132 | (actions editor IdeActions/ACTION_EDITOR_ENTER)
133 | new-state))
134 | ((:shift \O)
135 | "New line above" :write :scroll
136 | [editor document caret]
137 | (do (-> (ihx-selection document caret)
138 | (ihx-move-relative! :lines -1)
139 | (ihx-move-line-end editor document)
140 | (ihx-apply-selection! document))
141 | (.setSelection caret (.getSelectionEnd caret) (.getSelectionEnd caret)))
142 | [project editor state]
143 | (let [new-state (into-insert-mode project state editor :dump-selections false)]
144 | (actions editor IdeActions/ACTION_EDITOR_ENTER)
145 | new-state))
146 | ((:shift \%)
147 | "Select whole buffer"
148 | [editor document] (select-buffer editor document))
149 | (\s
150 | "Select in selections"
151 | [project editor document] (select-in-selections project editor document))
152 | (Character/isDigit
153 | "Add prefix arg" :keep-prefix
154 | [char state] (update state :prefix (fnil conj []) char))
155 | (\d
156 | "Delete selections" :undoable :write
157 | [state editor document]
158 | (delete-selections state editor document))
159 | (\c
160 | "Replace selections" :write
161 | [state project editor document]
162 | (replace-selections state project editor document))
163 | (\a
164 | "Append to selections"
165 | [document caret]
166 | (-> (ihx-selection document caret)
167 | ihx-make-forward
168 | (ihx-apply-selection! document))
169 | [project editor state document]
170 | (let [new-state (into-insert-mode project state editor :insertion-kind :append)]
171 | (actions editor IdeActions/ACTION_EDITOR_MOVE_CARET_RIGHT)
172 | new-state))
173 | ((:shift \A)
174 | "Append to line"
175 | [editor document caret]
176 | (-> (ihx-selection document caret)
177 | (ihx-move-line-end editor document)
178 | ihx-shrink-selection
179 | (ihx-apply-selection! document))
180 | [project editor state]
181 | (into-insert-mode project state editor))
182 | (\i
183 | "Prepend to selections"
184 | [document caret]
185 | (-> (ihx-selection document caret)
186 | ihx-make-backward
187 | (ihx-apply-selection! document))
188 | [project editor state]
189 | (into-insert-mode project state editor))
190 | ((:shift \I)
191 | "Prepend to lines"
192 | [editor document caret]
193 | (-> (ihx-selection document caret)
194 | (ihx-move-line-start editor document)
195 | ihx-shrink-selection
196 | (ihx-apply-selection! document))
197 | [project editor state]
198 | (into-insert-mode project state editor))
199 | ((:or (:alt \;) (:alt \u2026))
200 | "Flip selection" :scroll
201 | [document caret] (-> (ihx-selection document caret)
202 | flip-selection
203 | (ihx-apply-selection! document)))
204 | ((:or (:alt \:) (:alt \u00DA))
205 | "Make selections forward"
206 | [document caret]
207 | (-> (ihx-selection document caret)
208 | ihx-make-forward
209 | (ihx-apply-selection! document)))
210 | (\;
211 | "Shrink selections to 1 char"
212 | [document caret]
213 | (-> (ihx-selection document caret)
214 | ihx-shrink-selection
215 | (ihx-apply-selection! document)))
216 | (\,
217 | "Drop all selections but primary"
218 | [editor] (keep-primary-selection editor))
219 | (\x
220 | "Select whole lines extending" :scroll
221 | [state editor document caret]
222 | (dotimes [_ (get-prefix state)]
223 | (-> (ihx-selection document caret)
224 | (ihx-select-lines editor document :extend true)
225 | (ihx-apply-selection! document))))
226 | ((:shift \X)
227 | "Select whole lines" :scroll
228 | [editor document caret]
229 | (-> (ihx-selection document caret)
230 | (ihx-select-lines editor document)
231 | (ihx-apply-selection! document)))
232 | ((:shift \C)
233 | "Add selections below"
234 | [state editor caret]
235 | (add-selection-below editor caret))
236 | ((:or (:ctrl \o) (:ctrl \u000f))
237 | "Jump backward"
238 | [state project editor document]
239 | (jumplist-backward! state project))
240 | ((:or (:ctrl \i) (:ctrl \u0009))
241 | "Jump forward"
242 | [state project editor document]
243 | (jumplist-forward! state project))
244 | ((:or (:ctrl \s) (:ctrl \u0013))
245 | "Add to jumplist"
246 | [state project document]
247 | (jumplist-add project state)))
248 |
249 | (:normal
250 | (\g "Goto mode" :keep-prefix [state] (assoc state :mode :goto))
251 | (\v "Selection mode" [state] (assoc state :mode :select))
252 | ((:or (:ctrl \d) (:ctrl \u0004))
253 | "Move down half page" :scroll
254 | [editor document caret]
255 | (let [n-lines (quot (get-editor-height editor) 2)]
256 | (-> (ihx-selection document caret)
257 | (ihx-move-relative! :lines n-lines)
258 | (ihx-shrink-selection)
259 | (ihx-apply-selection! document))))
260 | ((:or (:ctrl \d) (:ctrl \u0015))
261 | "Move up half page extending" :scroll
262 | [editor document caret]
263 | (let [n-lines (quot (get-editor-height editor) 2)]
264 | (-> (ihx-selection document caret)
265 | (ihx-move-relative! :lines (- n-lines))
266 | (ihx-shrink-selection)
267 | (ihx-apply-selection! document))))
268 | (\p
269 | "Paste" :undoable :write
270 | [state editor document]
271 | (paste-register (:registers state) editor document :select true))
272 | (\w
273 | "Select word forward" :scroll
274 | [state editor document caret]
275 | (dotimes [_ (get-prefix state)]
276 | (-> (ihx-selection document caret)
277 | (ihx-word-forward! editor)
278 | (ihx-apply-selection! document))))
279 | ((:shift \W)
280 | "Select WORD forward" :scroll
281 | [state editor document caret]
282 | (dotimes [_ (get-prefix state)]
283 | (-> (ihx-selection document caret)
284 | (ihx-long-word-forward! document false)
285 | (ihx-apply-selection! document))))
286 | (\e
287 | "Select word end" :scroll
288 | [state editor document caret]
289 | (dotimes [_ (get-prefix state)]
290 | (-> (ihx-selection document caret)
291 | (ihx-word-end! editor)
292 | (ihx-apply-selection! document))))
293 | ((:shift \E)
294 | "Select WORD end" :scroll
295 | [state document caret]
296 | (dotimes [_ (get-prefix state)]
297 | (-> (ihx-selection document caret)
298 | (ihx-long-word-end! document false)
299 | (ihx-apply-selection! document))))
300 | (\b
301 | "Select word backward" :scroll
302 | [state document editor caret]
303 | (dotimes [_ (get-prefix state)]
304 | (-> (ihx-selection document caret)
305 | (ihx-word-backward! editor)
306 | (ihx-apply-selection! document))))
307 | ((:shift \B)
308 | "Select WORD backward" :scroll
309 | [state document caret]
310 | (dotimes [_ (get-prefix state)]
311 | (-> (ihx-selection document caret)
312 | (ihx-long-word-backward! document false)
313 | (ihx-apply-selection! document))))
314 | ((:or \j KeyEvent/VK_DOWN)
315 | "Move carets down" :scroll
316 | [state document caret]
317 | (dotimes [_ (get-prefix state)]
318 | (-> (ihx-selection document caret)
319 | (ihx-move-relative! :lines 1)
320 | ihx-shrink-selection
321 | (ihx-apply-selection-preserving document))))
322 | ((:or \k KeyEvent/VK_UP)
323 | "Move carets up" :scroll
324 | [state document caret]
325 | (dotimes [_ (get-prefix state)]
326 | (-> (ihx-selection document caret)
327 | (ihx-move-relative! :lines -1)
328 | ihx-shrink-selection
329 | (ihx-apply-selection-preserving document))))
330 | ((:or \h KeyEvent/VK_LEFT)
331 | "Move carets left" :scroll
332 | [state document caret]
333 | (-> (ihx-selection document caret)
334 | (ihx-move-backward (get-prefix state))
335 | ihx-shrink-selection
336 | (ihx-apply-selection! document)))
337 | ((:or \l KeyEvent/VK_RIGHT)
338 | "Move carets right" :scroll
339 | [state document caret]
340 | (-> (ihx-selection document caret)
341 | (ihx-move-forward (get-prefix state))
342 | ihx-shrink-selection
343 | (ihx-apply-selection! document)))
344 | (KeyEvent/VK_HOME
345 | "Move carets to line start" :scroll
346 | [editor document caret]
347 | (-> (ihx-selection document caret)
348 | (ihx-move-line-start editor document)
349 | ihx-shrink-selection
350 | (ihx-apply-selection! document)))
351 | (KeyEvent/VK_END
352 | "Move carets to line end" :scroll
353 | [editor document caret]
354 | (-> (ihx-selection document caret)
355 | (ihx-move-line-end editor document)
356 | (ihx-move-backward 1)
357 | ihx-shrink-selection
358 | (ihx-apply-selection! document)))
359 | ((:shift \G)
360 | "Move to line number" :scroll :jumplist-add
361 | [state editor document]
362 | (do (-> (ihx-move-caret-line-n editor document (get-prefix state))
363 | ihx-shrink-selection
364 | (ihx-apply-selection! document))
365 | (assoc state :mode :normal)))
366 | (\m
367 | "Match menu"
368 | [state] (assoc state :mode :match)))
369 |
370 | (:select
371 | (\g
372 | "Goto mode extending" :keep-prefix
373 | [state] (assoc state :mode :select-goto))
374 | (\v
375 | "Back to normal mode" [state] (assoc state :mode :normal))
376 | ((:or (:ctrl \d) (:ctrl \u0004))
377 | "Move down half page extending" :scroll
378 | [editor document caret]
379 | (let [n-lines (quot (get-editor-height editor) 2)]
380 | (-> (ihx-selection document caret)
381 | (ihx-move-relative! :lines n-lines)
382 | (ihx-apply-selection! document))))
383 | ((:or (:ctrl \d) (:ctrl \u0015))
384 | "Move up half page extending" :scroll
385 | [editor document caret]
386 | (let [n-lines (quot (get-editor-height editor) 2)]
387 | (-> (ihx-selection document caret)
388 | (ihx-move-relative! :lines (- n-lines))
389 | (ihx-apply-selection! document))))
390 | (\p
391 | "Paste" :undoable :write
392 | [state editor document]
393 | (paste-register (:registers state) editor document))
394 | (\w
395 | "Select word forward extending" :scroll
396 | [state document editor caret]
397 | (dotimes [_ (get-prefix state)]
398 | (-> (ihx-selection document caret)
399 | (ihx-word-forward-extending! editor)
400 | (ihx-apply-selection! document))))
401 | ((:shift \W)
402 | "Select WORD forward extending" :scroll
403 | [state editor document caret]
404 | (dotimes [_ (get-prefix state)]
405 | (-> (ihx-selection document caret)
406 | (ihx-long-word-forward! document true)
407 | (ihx-apply-selection! document))))
408 | (\e
409 | "Select word end extending" :scroll
410 | [state document editor caret]
411 | (dotimes [_ (get-prefix state)]
412 | (-> (ihx-selection document caret)
413 | (ihx-word-end-extending! editor)
414 | (ihx-apply-selection! document))))
415 | ((:shift \E)
416 | "Select WORD end extending" :scroll
417 | [state editor document caret]
418 | (dotimes [_ (get-prefix state)]
419 | (-> (ihx-selection document caret)
420 | (ihx-long-word-end! document true)
421 | (ihx-apply-selection! document))))
422 | (\b
423 | "Select word backward extending" :scroll
424 | [state document editor caret]
425 | (dotimes [_ (get-prefix state)]
426 | (-> (ihx-selection document caret)
427 | (ihx-word-backward-extending! editor)
428 | (ihx-apply-selection! document))))
429 | ((:shift \B)
430 | "Select WORD backward extending" :scroll
431 | [state editor document caret]
432 | (dotimes [_ (get-prefix state)]
433 | (-> (ihx-selection document caret)
434 | (ihx-long-word-backward! document true)
435 | (ihx-apply-selection! document))))
436 | ((:or \j KeyEvent/VK_DOWN)
437 | "Move carets down extending" :scroll
438 | [state document caret]
439 | (dotimes [_ (get-prefix state)]
440 | (-> (ihx-selection document caret)
441 | (ihx-move-relative! :lines 1)
442 | (ihx-apply-selection-preserving document))))
443 | ((:or \k KeyEvent/VK_UP)
444 | "Move carets up extending" :scroll
445 | [state document caret]
446 | (dotimes [_ (get-prefix state)]
447 | (-> (ihx-selection document caret)
448 | (ihx-move-relative! :lines -1)
449 | (ihx-apply-selection-preserving document))))
450 | ((:or \h KeyEvent/VK_LEFT)
451 | "Move carets left extending" :scroll
452 | [state document caret]
453 | (-> (ihx-selection document caret)
454 | (ihx-move-backward (get-prefix state))
455 | (ihx-apply-selection! document)))
456 | ((:or \l KeyEvent/VK_RIGHT)
457 | "Move carets right extending" :scroll
458 | [state document caret]
459 | (-> (ihx-selection document caret)
460 | (ihx-move-forward (get-prefix state))
461 | (ihx-apply-selection! document)))
462 | (KeyEvent/VK_HOME
463 | "Move carets to line start" :scroll
464 | [editor document caret]
465 | (-> (ihx-selection document caret)
466 | (ihx-move-line-start editor document)
467 | (ihx-apply-selection! document)))
468 | (KeyEvent/VK_END
469 | "Move carets to line end" :scroll
470 | [editor document caret]
471 | (-> (ihx-selection document caret)
472 | (ihx-move-line-end editor document)
473 | (ihx-move-backward 1)
474 | (ihx-apply-selection! document)))
475 | ((:shift \G)
476 | "Move to line number" :scroll :jumplist-add
477 | [state editor document]
478 | (do (-> (ihx-move-caret-line-n editor document (get-prefix state))
479 | (ihx-apply-selection! document))
480 | (assoc state :mode :select)))
481 | (\m
482 | "Match menu"
483 | [state] (assoc state :mode :select-match)))
484 |
485 | (:goto
486 | (Character/isDigit
487 | "Add prefix arg" :keep-prefix [char state] (update state :prefix conj char))
488 | (\d
489 | "Goto declaration" :jumplist-add
490 | [editor]
491 | (actions editor IdeActions/ACTION_GOTO_DECLARATION)
492 | [state] (assoc state :mode :normal))
493 | (\n
494 | "Next tab"
495 | [editor]
496 | (actions editor IdeActions/ACTION_NEXT_TAB)
497 | [state]
498 | (assoc state :mode :normal))
499 | (\p
500 | "Previous tab"
501 | [editor]
502 | (actions editor IdeActions/ACTION_PREVIOUS_TAB)
503 | [state]
504 | (assoc state :mode :normal))
505 | (\h
506 | "Move carets to line start" :scroll
507 | [editor document caret]
508 | (-> (ihx-selection document caret)
509 | (ihx-move-line-start editor document)
510 | ihx-shrink-selection
511 | (ihx-apply-selection! document))
512 | [state] (assoc state :mode :normal))
513 | (\l
514 | "Move carets to line end" :scroll
515 | [editor document caret]
516 | (-> (ihx-selection document caret)
517 | (ihx-move-line-end editor document)
518 | (ihx-move-backward 1)
519 | ihx-shrink-selection
520 | (ihx-apply-selection! document))
521 | [state] (assoc state :mode :normal))
522 | (\g
523 | "Move to line number" :scroll :jumplist-add
524 | [state editor document]
525 | (do (-> (ihx-move-caret-line-n editor document (get-prefix state))
526 | ihx-shrink-selection
527 | (ihx-apply-selection! document))
528 | (assoc state :mode :normal)))
529 | (\e
530 | "Move to file end" :scroll :jumplist-add
531 | [state editor document]
532 | (do (-> (ihx-move-file-end editor document)
533 | ihx-shrink-selection
534 | (ihx-apply-selection! document))
535 | (assoc state :mode :normal)))
536 | (_ [state] (assoc state :mode :normal)))
537 |
538 | (:select-goto
539 | (Character/isDigit
540 | "Add prefix arg" :keep-prefix [char state] (update state :prefix conj char))
541 | (\h
542 | "Move carets to line start extending" :scroll
543 | [editor document caret]
544 | (-> (ihx-selection document caret)
545 | (ihx-move-line-start editor document)
546 | (ihx-apply-selection! document))
547 | [state] (assoc state :mode :select))
548 | (\l
549 | "Move carets to line end extending" :scroll
550 | [editor document caret]
551 | (-> (ihx-selection document caret)
552 | (ihx-move-line-end editor document)
553 | (ihx-apply-selection! document))
554 | [state] (assoc state :mode :select))
555 | (\g
556 | "Move to line number" :scroll :jumplist-add
557 | [state editor document]
558 | (do (-> (ihx-move-caret-line-n editor document (get-prefix state))
559 | (ihx-apply-selection! document))
560 | (assoc state :mode :select)))
561 | (\e
562 | "Move to file end" :scroll :jumplist-add
563 | [state editor document]
564 | (do (-> (ihx-move-file-end editor document)
565 | (ihx-apply-selection! document))
566 | (assoc state :mode :select)))
567 | (_ [state] (assoc state :mode :select)))
568 |
569 | (:space
570 | (\f
571 | "File finder"
572 | [state project editor]
573 | (do
574 | (search-file-name project editor)
575 | (assoc state :mode :normal)))
576 | (\r
577 | "Rename symbol"
578 | [project editor document state]
579 | (do
580 | (actions editor IdeActions/ACTION_RENAME)
581 | (assoc state :mode :normal)))
582 | (\/
583 | "Global search"
584 | [state project editor]
585 | (let [manager (.. SearchEverywhereManager (getInstance project))
586 | data-context (.getDataContext editor)
587 | action-event (AnActionEvent/createFromDataContext
588 | ActionPlaces/KEYBOARD_SHORTCUT nil data-context)]
589 | (.show manager "TextSearchContributor" nil action-event)
590 | (assoc state :mode :normal))))
591 |
592 | (:view
593 | ((:or \z \c)
594 | "Center screen"
595 | [editor state]
596 | (let [caret (.. editor getCaretModel getPrimaryCaret)
597 | scrolling-model (.getScrollingModel editor)]
598 | (.scrollTo scrolling-model
599 | (.offsetToLogicalPosition editor (.. caret getOffset))
600 | ScrollType/CENTER)
601 | (assoc state :mode (:previous-mode state)))))
602 |
603 | (:window
604 | (\v
605 | "Split vertical"
606 | [editor] (actions editor "SplitVertically")
607 | [state] (assoc state :mode :normal))
608 | ((:or \w (:ctrl \u0017))
609 | "Switch split"
610 | [editor] (actions editor "NextSplitter")
611 | [state] (assoc state :mode :normal))
612 | (\o
613 | [editor] (actions editor "UnsplitAll")
614 | [state] (assoc state :mode :normal))
615 | (_ [state] (assoc state :mode :normal)))
616 |
617 | (:insert
618 | ((:or (:ctrl \n) (:ctrl \u000e))
619 | "Select next completion item"
620 | [state editor event]
621 | (when (= (.getID event) KeyEvent/KEY_PRESSED)
622 | (when-let [^LookupImpl lookup (LookupManager/getActiveLookup editor)]
623 | (.setSelectedIndex lookup (inc (.getSelectedIndex lookup)))
624 | state)))
625 | ((:or (:ctrl \p) (:ctrl \u0010))
626 | "Select next completion item"
627 | [state editor event]
628 | (when (= (.getID event) KeyEvent/KEY_PRESSED)
629 | (when-let [^LookupImpl lookup (LookupManager/getActiveLookup editor)]
630 | (.setSelectedIndex lookup (dec (.getSelectedIndex lookup)))
631 | state)))
632 | (_ [state] (assoc state :pass true)))
633 |
634 |
635 | (:match
636 | (\i
637 | [state] (assoc state :mode :match-inside))
638 | (\a
639 | [state] (assoc state :mode :match-around)))
640 |
641 | (:select-match
642 | (\i
643 | [state] (assoc state :mode :select-match-inside))
644 | (\a
645 | [state] (assoc state :mode :select-match-around)))
646 |
647 | (:match-inside
648 | (_
649 | "Select inside"
650 | [document caret char]
651 | (-> (ihx-selection document caret)
652 | (ihx-select-inside document char)
653 | (ihx-apply-selection! document))
654 | [state] (assoc state :mode :normal)))
655 |
656 | (:select-match-inside
657 | (_
658 | "Select inside"
659 | [document caret char]
660 | (-> (ihx-selection document caret)
661 | (ihx-select-inside document char)
662 | (ihx-apply-selection! document))
663 | [state] (assoc state :mode :select)))
664 |
665 | (:match-around
666 | (_
667 | "Select around"
668 | [document caret char]
669 | (-> (ihx-selection document caret)
670 | (ihx-select-around document char)
671 | (ihx-apply-selection! document))
672 | [state] (assoc state :mode :normal)))
673 |
674 | (:select-match-around
675 | (_
676 | "Select around"
677 | [document caret char]
678 | (-> (ihx-selection document caret)
679 | (ihx-select-around document char)
680 | (ihx-apply-selection! document))))
681 |
682 | ((:or :match :select-match)
683 | (\s
684 | [state] (assoc state :mode :match-surround-add))
685 | (\d
686 | [state] (assoc state :mode :match-surround-delete))
687 | (\r
688 | [state] (assoc state :mode :match-find)))
689 |
690 | (:match
691 | (\m
692 | "Goto matching bracket"
693 | [document caret]
694 | (-> (ihx-selection document caret)
695 | (ihx-goto-matching document)
696 | ihx-shrink-selection
697 | (ihx-apply-selection! document))
698 | [state] (assoc state :mode :normal)))
699 |
700 | (:select-match
701 | (\m
702 | "Goto matching bracket"
703 | [document caret]
704 | (-> (ihx-selection document caret)
705 | (ihx-goto-matching document)
706 | (ihx-apply-selection! document))
707 | [state] (assoc state :mode :select)))
708 |
709 | (:match-surround-add
710 | (_
711 | "Surround add" :write
712 | [document caret char]
713 | (-> (ihx-selection document caret)
714 | (ihx-surround-add document char)
715 | (ihx-apply-selection! document))
716 | [state] (assoc state :mode :normal)))
717 |
718 | (:match-surround-delete
719 | (_
720 | "Surround delete" :write
721 | [document caret char]
722 | (-> (ihx-selection document caret)
723 | (ihx-surround-delete document char)
724 | (ihx-apply-selection! document))
725 | [state] (assoc state :mode :normal)))
726 |
727 | (:match-find
728 | (_
729 | "Find the matching brackets"
730 | [state editor document char]
731 | (ihx-surround-find state editor document char)))
732 |
733 | (:match-replace
734 | (_
735 | "Replace the matching brackets" :write
736 | [state editor document char]
737 | (ihx-surround-replace state editor document char))))
738 |
739 |
740 | (defn handle-editor-event
741 | [project ^EditorImpl editor ^KeyEvent event]
742 | (let [project-state (or (get @state-atom project) {:mode :normal})
743 | mode (:mode project-state)
744 | debounce (:debounce project-state)
745 | result-fn (partial editor-handler project project-state editor event)
746 | result
747 | (try
748 | (if (= mode :insert)
749 | (cond
750 | (and (= (.getID event) KeyEvent/KEY_PRESSED)
751 | (= (.getKeyCode event) KeyEvent/VK_ESCAPE)) (result-fn)
752 | (and (not= (.getID event) KeyEvent/KEY_PRESSED)
753 | (= (.getKeyCode event) KeyEvent/VK_ESCAPE)) nil
754 | (and debounce (= (.getID event) KeyEvent/KEY_TYPED))
755 | (assoc project-state :debounce false)
756 | :else (result-fn))
757 | (if (and (= (.getID event) KeyEvent/KEY_PRESSED)
758 | (not (#{KeyEvent/VK_SHIFT KeyEvent/VK_CONTROL KeyEvent/VK_ALT KeyEvent/VK_META} (.getKeyCode event))))
759 | (result-fn)
760 | nil))
761 | (catch StartMarkAction$AlreadyStartedException e
762 | (quit-insert-mode project project-state (.getDocument editor))
763 | (throw e)))]
764 | (cond
765 | (:pass result) false
766 | (map? result) (do
767 | (.consume event)
768 | (let [new-state (merge project-state result)]
769 | (swap! state-atom assoc project new-state)
770 | (ui/update-mode-panel! project new-state))
771 | true)
772 | :default (do
773 | (.consume event)
774 | true))))
775 |
--------------------------------------------------------------------------------