| 1 | (defpackage :my (:use :cl)) |
|---|
| 2 | (in-package :my) |
|---|
| 3 | |
|---|
| 4 | ;; runtime-class.lisp is a part of ABCL, but it is excluded from ABCL build, |
|---|
| 5 | ;; because it requires asm.jar to be present in classpath during the build. |
|---|
| 6 | ;; |
|---|
| 7 | ;; The functionality it provides is necessary for dynamic creation of |
|---|
| 8 | ;; new java classes from Lisp (in particular for the |
|---|
| 9 | ;; NEW-CLASS macro of jfli ABCL port) |
|---|
| 10 | (load (concatenate 'string abclidea:*lisp-dir* "org/armedbear/lisp/runtime-class.lisp")) |
|---|
| 11 | |
|---|
| 12 | ;; Load jfli |
|---|
| 13 | (load (concatenate 'string abclidea:*lisp-dir* "jfli-abcl/jfli-abcl.lisp")) |
|---|
| 14 | |
|---|
| 15 | (use-package :jfli) |
|---|
| 16 | |
|---|
| 17 | ;; "Import" java classes we use. |
|---|
| 18 | ;; |
|---|
| 19 | ;; You may produce DEF-JAVA-CLASS forms for all the IDEA API classes automatically: |
|---|
| 20 | ;; |
|---|
| 21 | ;; (jfli:dump-wrapper-defs-to-file (concatenate 'string abclidea:*lisp-dir* "idea-api.lisp") |
|---|
| 22 | ;; (jfli:get-jar-classnames "path/to/idea/openapi.jar" |
|---|
| 23 | ;; "com/intellij")) |
|---|
| 24 | ;; |
|---|
| 25 | ;; |
|---|
| 26 | ;; In result they will be stored in idea-api.lisp file. |
|---|
| 27 | ;; |
|---|
| 28 | ;; But we do it manually, because there are not so many classes we use. |
|---|
| 29 | |
|---|
| 30 | (def-java-class "com.intellij.openapi.ui.Messages") |
|---|
| 31 | (use-package "com.intellij.openapi.ui") |
|---|
| 32 | |
|---|
| 33 | (def-java-class "com.intellij.openapi.application.ModalityState") |
|---|
| 34 | (def-java-class "com.intellij.openapi.application.Application") |
|---|
| 35 | (def-java-class "com.intellij.openapi.application.ApplicationManager") |
|---|
| 36 | (use-package "com.intellij.openapi.application") |
|---|
| 37 | |
|---|
| 38 | (def-java-class "com.intellij.openapi.actionSystem.AnAction") |
|---|
| 39 | (def-java-class "com.intellij.openapi.actionSystem.AnActionEvent") |
|---|
| 40 | (def-java-class "com.intellij.openapi.actionSystem.ActionManager") |
|---|
| 41 | (def-java-class "com.intellij.openapi.actionSystem.DefaultActionGroup") |
|---|
| 42 | (def-java-class "com.intellij.openapi.actionSystem.CustomShortcutSet") |
|---|
| 43 | (def-java-class "com.intellij.openapi.actionSystem.Shortcut") |
|---|
| 44 | (def-java-class "com.intellij.openapi.actionSystem.KeyboardShortcut") |
|---|
| 45 | (def-java-class "com.intellij.openapi.actionSystem.CustomShortcutSet") |
|---|
| 46 | (use-package "com.intellij.openapi.actionSystem") |
|---|
| 47 | |
|---|
| 48 | (def-java-class "com.intellij.openapi.ide.CopyPasteManager") |
|---|
| 49 | (use-package "com.intellij.openapi.ide") |
|---|
| 50 | |
|---|
| 51 | (def-java-class "com.intellij.openapi.keymap.KeymapManager") |
|---|
| 52 | (def-java-class "com.intellij.openapi.keymap.Keymap") |
|---|
| 53 | (use-package "com.intellij.openapi.keymap") |
|---|
| 54 | |
|---|
| 55 | (def-java-class "com.intellij.openapi.project.ProjectManager") |
|---|
| 56 | (use-package "com.intellij.openapi.project") |
|---|
| 57 | |
|---|
| 58 | (def-java-class "com.intellij.openapi.editor.Editor") |
|---|
| 59 | (def-java-class "com.intellij.openapi.editor.Document") |
|---|
| 60 | (def-java-class "com.intellij.openapi.editor.SelectionModel") |
|---|
| 61 | (use-package "com.intellij.openapi.editor") |
|---|
| 62 | |
|---|
| 63 | (def-java-class "com.intellij.openapi.fileEditor.FileEditorManager") |
|---|
| 64 | (def-java-class "com.intellij.openapi.fileEditor.FileEditor") |
|---|
| 65 | (def-java-class "com.intellij.openapi.fileEditor.TextEditor") |
|---|
| 66 | (use-package "com.intellij.openapi.fileEditor") |
|---|
| 67 | |
|---|
| 68 | (def-java-class "com.intellij.openapi.command.CommandProcessor") |
|---|
| 69 | (def-java-class "com.intellij.openapi.command.CommandAdapter") |
|---|
| 70 | (def-java-class "com.intellij.openapi.command.CommandEvent") |
|---|
| 71 | (use-package "com.intellij.openapi.command") |
|---|
| 72 | |
|---|
| 73 | (def-java-class "com.intellij.openapi.wm.WindowManager") |
|---|
| 74 | (def-java-class "com.intellij.openapi.wm.StatusBar") |
|---|
| 75 | (use-package "com.intellij.openapi.wm") |
|---|
| 76 | |
|---|
| 77 | (def-java-class "java.lang.Runnable") |
|---|
| 78 | (def-java-class "java.lang.Thread") |
|---|
| 79 | (def-java-class "java.lang.Object") |
|---|
| 80 | (def-java-class "java.lang.Class") |
|---|
| 81 | (def-java-class "java.lang.String") |
|---|
| 82 | (use-package "java.lang") |
|---|
| 83 | |
|---|
| 84 | (def-java-class "java.awt.datatransfer.Transferable") |
|---|
| 85 | (def-java-class "java.awt.datatransfer.DataFlavor") |
|---|
| 86 | (use-package "java.awt.datatransfer") |
|---|
| 87 | |
|---|
| 88 | (def-java-class "javax.swing.KeyStroke") |
|---|
| 89 | (use-package "javax.swing") |
|---|
| 90 | |
|---|
| 91 | (define-condition action-is-not-applicable () |
|---|
| 92 | ((why :initarg :why :reader why)) |
|---|
| 93 | (:report (lambda (condition stream) |
|---|
| 94 | (format stream "Action is not applicable: ~A" (why condition))))) |
|---|
| 95 | |
|---|
| 96 | (defun cur-prj () |
|---|
| 97 | (let ((all-prjs (projectmanager.getopenprojects (projectmanager.getinstance)))) |
|---|
| 98 | (when (> (jlength all-prjs) 0) |
|---|
| 99 | (jref all-prjs 0)))) |
|---|
| 100 | |
|---|
| 101 | (defun cur-prj-safe () |
|---|
| 102 | (or (cur-prj) (error 'action-is-not-applicable :why "no current project"))) |
|---|
| 103 | |
|---|
| 104 | (defun cur-editor (prj) |
|---|
| 105 | (fileeditormanager.getselectedtexteditor (fileeditormanager.getinstance prj))) |
|---|
| 106 | |
|---|
| 107 | (defun cur-editor-safe (prj) |
|---|
| 108 | (or (cur-editor prj) |
|---|
| 109 | (error 'action-is-not-applicable |
|---|
| 110 | :why "no text editor is selected"))) |
|---|
| 111 | |
|---|
| 112 | ;; region object |
|---|
| 113 | (defun make-region (start end) |
|---|
| 114 | (cons start end)) |
|---|
| 115 | |
|---|
| 116 | (defun region-start (region) |
|---|
| 117 | (car region)) |
|---|
| 118 | |
|---|
| 119 | (defun region-end (region) |
|---|
| 120 | (cdr region)) |
|---|
| 121 | |
|---|
| 122 | (defun get-sel-region() |
|---|
| 123 | "Selection in the currently active editor" |
|---|
| 124 | (let* ((cur-prj (cur-prj-safe)) |
|---|
| 125 | (cur-editor (cur-editor-safe cur-prj)) |
|---|
| 126 | (sel-model (editor.getselectionmodel cur-editor))) |
|---|
| 127 | (make-region |
|---|
| 128 | (selectionmodel.getselectionstart sel-model) |
|---|
| 129 | (selectionmodel.getselectionend sel-model)))) |
|---|
| 130 | |
|---|
| 131 | (defun replace-region (replacement-text region) |
|---|
| 132 | "Replace text in the curently active editor" |
|---|
| 133 | (let* ((cur-prj (cur-prj-safe)) |
|---|
| 134 | (cur-editor (cur-editor-safe cur-prj)) |
|---|
| 135 | (cur-doc (editor.getdocument cur-editor))) |
|---|
| 136 | (document.replacestring cur-doc |
|---|
| 137 | (region-start region) |
|---|
| 138 | (region-end region) |
|---|
| 139 | replacement-text))) |
|---|
| 140 | |
|---|
| 141 | (defvar *yank-index* 0 |
|---|
| 142 | "Index of clipboard item that will be pasted by the next yank or |
|---|
| 143 | yank-pop operation \(similar to kill-ring-yank-pointer in Emacs\).") |
|---|
| 144 | |
|---|
| 145 | (defvar *yank-region* nil |
|---|
| 146 | "Region of text that was inserted by previous yank or yank-pop command, |
|---|
| 147 | and that must be replaced by next yank-pop.") |
|---|
| 148 | |
|---|
| 149 | (defvar *yank-undo-id* 0 |
|---|
| 150 | "Yank following by a sequence of yank-pop must be considered as a |
|---|
| 151 | single action by undo mechanism. This variable is unique identifier |
|---|
| 152 | of such an compound action.") |
|---|
| 153 | |
|---|
| 154 | (defun get-yank-text (&optional (index 0)) |
|---|
| 155 | (let ((all-contents (copypastemanager.getallcontents (copypastemanager.getinstance))) |
|---|
| 156 | content) |
|---|
| 157 | (when (zerop (jlength all-contents)) |
|---|
| 158 | (RETURN-FROM get-yank-tex nil)) |
|---|
| 159 | (setf content (jref all-contents (mod index (jlength all-contents)))) |
|---|
| 160 | (transferable.gettransferdata content (dataflavor.stringflavor)))) |
|---|
| 161 | |
|---|
| 162 | (defun get-yank-text-safe (&optional (index 0)) |
|---|
| 163 | (or (get-yank-text index) |
|---|
| 164 | (error 'action-is-not-applicable :why "clipboard is empty"))) |
|---|
| 165 | |
|---|
| 166 | (defun next-yank-region (cur-selection-region replacement-text) |
|---|
| 167 | (make-region (region-start cur-selection-region) |
|---|
| 168 | (+ (region-start cur-selection-region) |
|---|
| 169 | (length (java:jobject-lisp-value replacement-text))))) |
|---|
| 170 | (defun yank() |
|---|
| 171 | (let ((sel-region (get-sel-region)) |
|---|
| 172 | (yank-text (get-yank-text-safe))) |
|---|
| 173 | (replace-region yank-text |
|---|
| 174 | sel-region) |
|---|
| 175 | (setf *yank-region* (next-yank-region sel-region |
|---|
| 176 | yank-text)) |
|---|
| 177 | (setf *yank-index* 1))) |
|---|
| 178 | |
|---|
| 179 | (defun make-runnable (fun) |
|---|
| 180 | (java:jinterface-implementation |
|---|
| 181 | "java.lang.Runnable" |
|---|
| 182 | "run" |
|---|
| 183 | ;; wrap FUN into lambda to allow it to be |
|---|
| 184 | ;; not only function objects, but also symbols |
|---|
| 185 | ;; (java:jinterface-implementation supports |
|---|
| 186 | ;; only function objects) |
|---|
| 187 | (lambda () (funcall fun)))) |
|---|
| 188 | |
|---|
| 189 | (defmacro runnable (&body body) |
|---|
| 190 | `(make-runnable (lambda () ,@body))) |
|---|
| 191 | |
|---|
| 192 | (defun run-write-action (fun) |
|---|
| 193 | (let ((app (applicationmanager.getapplication)) |
|---|
| 194 | (runnable (make-runnable fun))) |
|---|
| 195 | (application.runwriteaction app runnable))) |
|---|
| 196 | |
|---|
| 197 | (defun exec-cmd (fun name group-id) |
|---|
| 198 | (commandprocessor.executecommand (commandprocessor.getinstance) |
|---|
| 199 | (cur-prj) |
|---|
| 200 | (make-runnable fun) |
|---|
| 201 | name |
|---|
| 202 | group-id)) |
|---|
| 203 | |
|---|
| 204 | ;; set status bar text |
|---|
| 205 | (defun set-status (status-text) |
|---|
| 206 | (statusbar.setinfo (windowmanager.getstatusbar |
|---|
| 207 | (windowmanager.getinstance) |
|---|
| 208 | (cur-prj)) |
|---|
| 209 | status-text)) |
|---|
| 210 | |
|---|
| 211 | (new-class |
|---|
| 212 | "MY.MyAction" ;; class name |
|---|
| 213 | anaction. ;; super class |
|---|
| 214 | |
|---|
| 215 | ;; constructors |
|---|
| 216 | ( |
|---|
| 217 | (((text "java.lang.String") (func "java.lang.Object")) |
|---|
| 218 | (super text) |
|---|
| 219 | (setf (myaction.func this) func)) |
|---|
| 220 | ) |
|---|
| 221 | |
|---|
| 222 | ;; methods |
|---|
| 223 | ( |
|---|
| 224 | ("actionPerformed" :void :public (action-event) |
|---|
| 225 | ;; It's usefull to setup a restart before |
|---|
| 226 | ;; calling FUNC. |
|---|
| 227 | ;; |
|---|
| 228 | ;; It helps when slime is connected to |
|---|
| 229 | ;; the IDEA and error happens |
|---|
| 230 | ;; during action execution. |
|---|
| 231 | ;; |
|---|
| 232 | ;; Slime debugger hooks the error, |
|---|
| 233 | ;; but as actions are invoked from |
|---|
| 234 | ;; idea UI event dispatching thread, |
|---|
| 235 | ;; no slime restarts are set |
|---|
| 236 | ;; and our restart is the only |
|---|
| 237 | ;; way to leave SLIME debugger. |
|---|
| 238 | (restart-case |
|---|
| 239 | (handler-case |
|---|
| 240 | (funcall (myaction.func this) action-event) |
|---|
| 241 | (action-is-not-applicable () |
|---|
| 242 | ;; NOTE: it is not guaranteed |
|---|
| 243 | ;; that execution will be passed to this |
|---|
| 244 | ;; handler, even if your code signals |
|---|
| 245 | ;; ACTION-IS-NOT-APPLICABLE. |
|---|
| 246 | ;; |
|---|
| 247 | ;; It's so because ABCL impements |
|---|
| 248 | ;; non local exits using java exceptions |
|---|
| 249 | ;; (org.armedbear.lisp.Go); if somewhere |
|---|
| 250 | ;; in the call stack below our HANDLER-CASE |
|---|
| 251 | ;; and above the SIGNAL there is a |
|---|
| 252 | ;; |
|---|
| 253 | ;; catch (Throwable) |
|---|
| 254 | ;; |
|---|
| 255 | ;; then ABCL's Go exception will be catched. |
|---|
| 256 | ;; |
|---|
| 257 | ;; catch (Throwable) is in partiular |
|---|
| 258 | ;; used by IDEA methods that accept Runnable |
|---|
| 259 | ;; (like CommandProcessor.executeCommand, |
|---|
| 260 | ;; Application.runWriteAction) |
|---|
| 261 | ;; |
|---|
| 262 | ;; But even despite that, HANDLER-CASE |
|---|
| 263 | ;; is useful, because ACTION-IS-NOT-APPLICABLE |
|---|
| 264 | ;; is not trapped by Slime debugger. |
|---|
| 265 | )) |
|---|
| 266 | (continue () |
|---|
| 267 | :report "Return from IDEA action" |
|---|
| 268 | nil))) |
|---|
| 269 | ) |
|---|
| 270 | |
|---|
| 271 | ;; fields |
|---|
| 272 | ( |
|---|
| 273 | ("func" "java.lang.Object" :public)) |
|---|
| 274 | ) |
|---|
| 275 | |
|---|
| 276 | (setf act-yank (myaction.new "yank" nil)) |
|---|
| 277 | (setf (myaction.func act-yank) |
|---|
| 278 | #'(lambda (action-event) |
|---|
| 279 | (declare (ignore action-event)) |
|---|
| 280 | (incf *yank-undo-id*) |
|---|
| 281 | (exec-cmd (lambda () |
|---|
| 282 | (run-write-action 'yank)) |
|---|
| 283 | "yank" |
|---|
| 284 | (format nil "yank-~A" *yank-undo-id*)))) |
|---|
| 285 | |
|---|
| 286 | (setf edit-menu (actionmanager.getaction (actionmanager.getinstance) "EditMenu")) |
|---|
| 287 | |
|---|
| 288 | (actionmanager.registeraction (actionmanager.getinstance) "yank" act-yank) |
|---|
| 289 | (defaultactiongroup.add edit-menu act-yank) |
|---|
| 290 | |
|---|
| 291 | ;;(actionmanager.unregisteraction (actionmanager.getinstance) "yank") |
|---|
| 292 | ;;(defaultactiongroup.remove edit-menu act-yank) |
|---|
| 293 | |
|---|
| 294 | ;; assign keyboard shortcut Ctrl-Y to our action |
|---|
| 295 | ;; (by default Ctrl-Y is used for delete-line operation in IDEA; |
|---|
| 296 | ;; override this by unregistering Ctrl-Y from delete-line) |
|---|
| 297 | (defun action-shortcut (anaction) |
|---|
| 298 | "The first element of AnAction.getShorcuts()" |
|---|
| 299 | (jref (customshortcutset.getshortcuts (anaction.getshortcutset anaction)) 0)) |
|---|
| 300 | |
|---|
| 301 | (defun remove-shortcut (keystroke-str) |
|---|
| 302 | "Unregister all the shortcuts specified by KEYSTROKE-STR |
|---|
| 303 | for all the actions in the active keymap. |
|---|
| 304 | Example \(REMOVE-SHORTCUT \"control Y\"\)" |
|---|
| 305 | (let* ((keymap (keymapmanager.getactivekeymap (keymapmanager.getinstance))) |
|---|
| 306 | (keystroke (keystroke.getkeystroke keystroke-str)) |
|---|
| 307 | (act-ids (keymap.getactionids keymap keystroke))) |
|---|
| 308 | (dotimes (i (jlength act-ids)) |
|---|
| 309 | (let ((shortcuts (keymap.getshortcuts keymap (jref act-ids i)))) |
|---|
| 310 | (dotimes (j (jlength shortcuts)) |
|---|
| 311 | (let ((shortcut (jref shortcuts j))) |
|---|
| 312 | (when (class.isinstance (class.forname "com.intellij.openapi.actionSystem.KeyboardShortcut") |
|---|
| 313 | shortcut) |
|---|
| 314 | (when (jeq (keyboardshortcut.getfirstkeystroke shortcut) |
|---|
| 315 | keystroke) |
|---|
| 316 | (keymap.removeshortcut keymap (jref act-ids i) shortcut))))))))) |
|---|
| 317 | |
|---|
| 318 | ;; this is to display shortcut correctly in the menu |
|---|
| 319 | (anaction.setshortcutset act-yank |
|---|
| 320 | (customshortcutset.new (keystroke.getkeystroke "control Y"))) |
|---|
| 321 | |
|---|
| 322 | ;; this is to make it actually fired when user presses the key combination |
|---|
| 323 | (remove-shortcut "control Y") |
|---|
| 324 | (keymap.addshortcut (keymapmanager.getactivekeymap (keymapmanager.getinstance)) |
|---|
| 325 | "yank" |
|---|
| 326 | (action-shortcut act-yank)) |
|---|
| 327 | |
|---|
| 328 | ;; yank-pop is allowed only if previous command was yank or yank-pop. |
|---|
| 329 | ;; Add a command listentener that clears *yank-region* when any |
|---|
| 330 | ;; other command is executed, and thus makes yank-pop impossible. |
|---|
| 331 | (new-class |
|---|
| 332 | "MY.MyCommandListener" ;; class name |
|---|
| 333 | commandadapter. ;; super class |
|---|
| 334 | |
|---|
| 335 | ;; constructors |
|---|
| 336 | () |
|---|
| 337 | |
|---|
| 338 | ;; methods |
|---|
| 339 | ( |
|---|
| 340 | ("commandFinished" :void :public (command-event) |
|---|
| 341 | (unless (member (java:jobject-lisp-value (commandevent.getcommandname |
|---|
| 342 | command-event)) |
|---|
| 343 | '("yank" "yank-pop") |
|---|
| 344 | :test #'string=) |
|---|
| 345 | (setf *yank-region* nil))) |
|---|
| 346 | ) |
|---|
| 347 | |
|---|
| 348 | ;; fields |
|---|
| 349 | () |
|---|
| 350 | ) |
|---|
| 351 | |
|---|
| 352 | (setf my-cmd-listener (mycommandlistener.new)) |
|---|
| 353 | (commandprocessor.addcommandlistener (commandprocessor.getinstance) |
|---|
| 354 | my-cmd-listener) |
|---|
| 355 | |
|---|
| 356 | ;; (actionmanager.unregisteraction (actionmanager.getinstance) "yank-pop") |
|---|
| 357 | ;; (defaultactiongroup.remove edit-menu act-yank-pop) |
|---|
| 358 | |
|---|
| 359 | (defun yank-pop () |
|---|
| 360 | (let ((yank-text (get-yank-text *yank-index*))) |
|---|
| 361 | (replace-region yank-text *yank-region*) |
|---|
| 362 | (setf *yank-region* (make-region (region-start *yank-region*) |
|---|
| 363 | (+ (region-start *yank-region*) |
|---|
| 364 | (string.length yank-text))))) |
|---|
| 365 | (incf *yank-index*)) |
|---|
| 366 | |
|---|
| 367 | (setf act-yank-pop (myaction.new "yank-pop" nil)) |
|---|
| 368 | (setf (myaction.func act-yank-pop) |
|---|
| 369 | #'(lambda (action-event) |
|---|
| 370 | (if *yank-region* |
|---|
| 371 | (exec-cmd (lambda () |
|---|
| 372 | (run-write-action 'yank-pop)) |
|---|
| 373 | "yank-pop" |
|---|
| 374 | (format nil "yank-~A" *yank-undo-id*)) |
|---|
| 375 | (set-status "Previous command was not a yank")))) |
|---|
| 376 | |
|---|
| 377 | (actionmanager.registeraction (actionmanager.getinstance) "yank-pop" act-yank-pop) |
|---|
| 378 | (defaultactiongroup.add edit-menu act-yank-pop) |
|---|
| 379 | |
|---|
| 380 | (anaction.setshortcutset act-yank-pop |
|---|
| 381 | (customshortcutset.new (keystroke.getkeystroke "alt Y"))) |
|---|
| 382 | |
|---|
| 383 | (keymap.addshortcut (keymapmanager.getactivekeymap (keymapmanager.getinstance)) |
|---|
| 384 | "yank-pop" |
|---|
| 385 | (action-shortcut act-yank-pop)) |
|---|
| 386 | |
|---|