├── 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 | 17 | 37 | 51 | 52 | 54 | 60 | 63 | 67 | 71 | 72 | 83 | 92 | 93 | 98 | 103 | 108 | 113 | 118 | 119 | 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 | IdeaHelix 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 | 17 | 37 | 51 | 52 | 54 | 60 | 63 | 67 | 71 | 72 | 83 | 92 | 93 | 98 | 103 | 108 | 113 | 118 | 123 | 124 | 125 | -------------------------------------------------------------------------------- /ideahelix_dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 37 | 51 | 52 | 54 | 60 | 63 | 67 | 71 | 72 | 83 | 92 | 93 | 98 | 103 | 108 | 113 | 118 | 123 | 124 | 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 | --------------------------------------------------------------------------------