(defpackage :my (:use :cl)) (in-package :my) ;; runtime-class.lisp is a part of ABCL, but it is excluded from ABCL build, ;; because it requires asm.jar to be present in classpath during the build. ;; ;; The functionality it provides is necessary for dynamic creation of ;; new java classes from Lisp (in particular for the ;; NEW-CLASS macro of jfli ABCL port) (load (concatenate 'string abclidea:*lisp-dir* "org/armedbear/lisp/runtime-class.lisp")) ;; Load jfli (load (concatenate 'string abclidea:*lisp-dir* "jfli-abcl/jfli-abcl.lisp")) (use-package :jfli) ;; "Import" java classes we use. ;; ;; You may produce DEF-JAVA-CLASS forms for all the IDEA API classes automatically: ;; ;; (jfli:dump-wrapper-defs-to-file (concatenate 'string abclidea:*lisp-dir* "idea-api.lisp") ;; (jfli:get-jar-classnames "path/to/idea/openapi.jar" ;; "com/intellij")) ;; ;; ;; In result they will be stored in idea-api.lisp file. ;; ;; But we do it manually, because there are not so many classes we use. (def-java-class "com.intellij.openapi.ui.Messages") (use-package "com.intellij.openapi.ui") (def-java-class "com.intellij.openapi.application.ModalityState") (def-java-class "com.intellij.openapi.application.Application") (def-java-class "com.intellij.openapi.application.ApplicationManager") (use-package "com.intellij.openapi.application") (def-java-class "com.intellij.openapi.actionSystem.AnAction") (def-java-class "com.intellij.openapi.actionSystem.AnActionEvent") (def-java-class "com.intellij.openapi.actionSystem.ActionManager") (def-java-class "com.intellij.openapi.actionSystem.DefaultActionGroup") (def-java-class "com.intellij.openapi.actionSystem.CustomShortcutSet") (def-java-class "com.intellij.openapi.actionSystem.Shortcut") (def-java-class "com.intellij.openapi.actionSystem.KeyboardShortcut") (def-java-class "com.intellij.openapi.actionSystem.CustomShortcutSet") (use-package "com.intellij.openapi.actionSystem") (def-java-class "com.intellij.openapi.ide.CopyPasteManager") (use-package "com.intellij.openapi.ide") (def-java-class "com.intellij.openapi.keymap.KeymapManager") (def-java-class "com.intellij.openapi.keymap.Keymap") (use-package "com.intellij.openapi.keymap") (def-java-class "com.intellij.openapi.project.ProjectManager") (use-package "com.intellij.openapi.project") (def-java-class "com.intellij.openapi.editor.Editor") (def-java-class "com.intellij.openapi.editor.Document") (def-java-class "com.intellij.openapi.editor.SelectionModel") (use-package "com.intellij.openapi.editor") (def-java-class "com.intellij.openapi.fileEditor.FileEditorManager") (def-java-class "com.intellij.openapi.fileEditor.FileEditor") (def-java-class "com.intellij.openapi.fileEditor.TextEditor") (use-package "com.intellij.openapi.fileEditor") (def-java-class "com.intellij.openapi.command.CommandProcessor") (def-java-class "com.intellij.openapi.command.CommandAdapter") (def-java-class "com.intellij.openapi.command.CommandEvent") (use-package "com.intellij.openapi.command") (def-java-class "com.intellij.openapi.wm.WindowManager") (def-java-class "com.intellij.openapi.wm.StatusBar") (use-package "com.intellij.openapi.wm") (def-java-class "java.lang.Runnable") (def-java-class "java.lang.Thread") (def-java-class "java.lang.Object") (def-java-class "java.lang.Class") (def-java-class "java.lang.String") (use-package "java.lang") (def-java-class "java.awt.datatransfer.Transferable") (def-java-class "java.awt.datatransfer.DataFlavor") (use-package "java.awt.datatransfer") (def-java-class "javax.swing.KeyStroke") (use-package "javax.swing") (define-condition action-is-not-applicable () ((why :initarg :why :reader why)) (:report (lambda (condition stream) (format stream "Action is not applicable: ~A" (why condition))))) (defun cur-prj () (let ((all-prjs (projectmanager.getopenprojects (projectmanager.getinstance)))) (when (> (jlength all-prjs) 0) (jref all-prjs 0)))) (defun cur-prj-safe () (or (cur-prj) (error 'action-is-not-applicable :why "no current project"))) (defun cur-editor (prj) (fileeditormanager.getselectedtexteditor (fileeditormanager.getinstance prj))) (defun cur-editor-safe (prj) (or (cur-editor prj) (error 'action-is-not-applicable :why "no text editor is selected"))) ;; region object (defun make-region (start end) (cons start end)) (defun region-start (region) (car region)) (defun region-end (region) (cdr region)) (defun get-sel-region() "Selection in the currently active editor" (let* ((cur-prj (cur-prj-safe)) (cur-editor (cur-editor-safe cur-prj)) (sel-model (editor.getselectionmodel cur-editor))) (make-region (selectionmodel.getselectionstart sel-model) (selectionmodel.getselectionend sel-model)))) (defun replace-region (replacement-text region) "Replace text in the curently active editor" (let* ((cur-prj (cur-prj-safe)) (cur-editor (cur-editor-safe cur-prj)) (cur-doc (editor.getdocument cur-editor))) (document.replacestring cur-doc (region-start region) (region-end region) replacement-text))) (defvar *yank-index* 0 "Index of clipboard item that will be pasted by the next yank or yank-pop operation \(similar to kill-ring-yank-pointer in Emacs\).") (defvar *yank-region* nil "Region of text that was inserted by previous yank or yank-pop command, and that must be replaced by next yank-pop.") (defvar *yank-undo-id* 0 "Yank following by a sequence of yank-pop must be considered as a single action by undo mechanism. This variable is unique identifier of such an compound action.") (defun get-yank-text (&optional (index 0)) (let ((all-contents (copypastemanager.getallcontents (copypastemanager.getinstance))) content) (when (zerop (jlength all-contents)) (RETURN-FROM get-yank-tex nil)) (setf content (jref all-contents (mod index (jlength all-contents)))) (transferable.gettransferdata content (dataflavor.stringflavor)))) (defun get-yank-text-safe (&optional (index 0)) (or (get-yank-text index) (error 'action-is-not-applicable :why "clipboard is empty"))) (defun next-yank-region (cur-selection-region replacement-text) (make-region (region-start cur-selection-region) (+ (region-start cur-selection-region) (length (java:jobject-lisp-value replacement-text))))) (defun yank() (let ((sel-region (get-sel-region)) (yank-text (get-yank-text-safe))) (replace-region yank-text sel-region) (setf *yank-region* (next-yank-region sel-region yank-text)) (setf *yank-index* 1))) (defun make-runnable (fun) (java:jinterface-implementation "java.lang.Runnable" "run" ;; wrap FUN into lambda to allow it to be ;; not only function objects, but also symbols ;; (java:jinterface-implementation supports ;; only function objects) (lambda () (funcall fun)))) (defmacro runnable (&body body) `(make-runnable (lambda () ,@body))) (defun run-write-action (fun) (let ((app (applicationmanager.getapplication)) (runnable (make-runnable fun))) (application.runwriteaction app runnable))) (defun exec-cmd (fun name group-id) (commandprocessor.executecommand (commandprocessor.getinstance) (cur-prj) (make-runnable fun) name group-id)) ;; set status bar text (defun set-status (status-text) (statusbar.setinfo (windowmanager.getstatusbar (windowmanager.getinstance) (cur-prj)) status-text)) (new-class "MY.MyAction" ;; class name anaction. ;; super class ;; constructors ( (((text "java.lang.String") (func "java.lang.Object")) (super text) (setf (myaction.func this) func)) ) ;; methods ( ("actionPerformed" :void :public (action-event) ;; It's usefull to setup a restart before ;; calling FUNC. ;; ;; It helps when slime is connected to ;; the IDEA and error happens ;; during action execution. ;; ;; Slime debugger hooks the error, ;; but as actions are invoked from ;; idea UI event dispatching thread, ;; no slime restarts are set ;; and our restart is the only ;; way to leave SLIME debugger. (restart-case (handler-case (funcall (myaction.func this) action-event) (action-is-not-applicable () ;; NOTE: it is not guaranteed ;; that execution will be passed to this ;; handler, even if your code signals ;; ACTION-IS-NOT-APPLICABLE. ;; ;; It's so because ABCL impements ;; non local exits using java exceptions ;; (org.armedbear.lisp.Go); if somewhere ;; in the call stack below our HANDLER-CASE ;; and above the SIGNAL there is a ;; ;; catch (Throwable) ;; ;; then ABCL's Go exception will be catched. ;; ;; catch (Throwable) is in partiular ;; used by IDEA methods that accept Runnable ;; (like CommandProcessor.executeCommand, ;; Application.runWriteAction) ;; ;; But even despite that, HANDLER-CASE ;; is useful, because ACTION-IS-NOT-APPLICABLE ;; is not trapped by Slime debugger. )) (continue () :report "Return from IDEA action" nil))) ) ;; fields ( ("func" "java.lang.Object" :public)) ) (setf act-yank (myaction.new "yank" nil)) (setf (myaction.func act-yank) #'(lambda (action-event) (declare (ignore action-event)) (incf *yank-undo-id*) (exec-cmd (lambda () (run-write-action 'yank)) "yank" (format nil "yank-~A" *yank-undo-id*)))) (setf edit-menu (actionmanager.getaction (actionmanager.getinstance) "EditMenu")) (actionmanager.registeraction (actionmanager.getinstance) "yank" act-yank) (defaultactiongroup.add edit-menu act-yank) ;;(actionmanager.unregisteraction (actionmanager.getinstance) "yank") ;;(defaultactiongroup.remove edit-menu act-yank) ;; assign keyboard shortcut Ctrl-Y to our action ;; (by default Ctrl-Y is used for delete-line operation in IDEA; ;; override this by unregistering Ctrl-Y from delete-line) (defun action-shortcut (anaction) "The first element of AnAction.getShorcuts()" (jref (customshortcutset.getshortcuts (anaction.getshortcutset anaction)) 0)) (defun remove-shortcut (keystroke-str) "Unregister all the shortcuts specified by KEYSTROKE-STR for all the actions in the active keymap. Example \(REMOVE-SHORTCUT \"control Y\"\)" (let* ((keymap (keymapmanager.getactivekeymap (keymapmanager.getinstance))) (keystroke (keystroke.getkeystroke keystroke-str)) (act-ids (keymap.getactionids keymap keystroke))) (dotimes (i (jlength act-ids)) (let ((shortcuts (keymap.getshortcuts keymap (jref act-ids i)))) (dotimes (j (jlength shortcuts)) (let ((shortcut (jref shortcuts j))) (when (class.isinstance (class.forname "com.intellij.openapi.actionSystem.KeyboardShortcut") shortcut) (when (jeq (keyboardshortcut.getfirstkeystroke shortcut) keystroke) (keymap.removeshortcut keymap (jref act-ids i) shortcut))))))))) ;; this is to display shortcut correctly in the menu (anaction.setshortcutset act-yank (customshortcutset.new (keystroke.getkeystroke "control Y"))) ;; this is to make it actually fired when user presses the key combination (remove-shortcut "control Y") (keymap.addshortcut (keymapmanager.getactivekeymap (keymapmanager.getinstance)) "yank" (action-shortcut act-yank)) ;; yank-pop is allowed only if previous command was yank or yank-pop. ;; Add a command listentener that clears *yank-region* when any ;; other command is executed, and thus makes yank-pop impossible. (new-class "MY.MyCommandListener" ;; class name commandadapter. ;; super class ;; constructors () ;; methods ( ("commandFinished" :void :public (command-event) (unless (member (java:jobject-lisp-value (commandevent.getcommandname command-event)) '("yank" "yank-pop") :test #'string=) (setf *yank-region* nil))) ) ;; fields () ) (setf my-cmd-listener (mycommandlistener.new)) (commandprocessor.addcommandlistener (commandprocessor.getinstance) my-cmd-listener) ;; (actionmanager.unregisteraction (actionmanager.getinstance) "yank-pop") ;; (defaultactiongroup.remove edit-menu act-yank-pop) (defun yank-pop () (let ((yank-text (get-yank-text *yank-index*))) (replace-region yank-text *yank-region*) (setf *yank-region* (make-region (region-start *yank-region*) (+ (region-start *yank-region*) (string.length yank-text))))) (incf *yank-index*)) (setf act-yank-pop (myaction.new "yank-pop" nil)) (setf (myaction.func act-yank-pop) #'(lambda (action-event) (if *yank-region* (exec-cmd (lambda () (run-write-action 'yank-pop)) "yank-pop" (format nil "yank-~A" *yank-undo-id*)) (set-status "Previous command was not a yank")))) (actionmanager.registeraction (actionmanager.getinstance) "yank-pop" act-yank-pop) (defaultactiongroup.add edit-menu act-yank-pop) (anaction.setshortcutset act-yank-pop (customshortcutset.new (keystroke.getkeystroke "alt Y"))) (keymap.addshortcut (keymapmanager.getactivekeymap (keymapmanager.getinstance)) "yank-pop" (action-shortcut act-yank-pop))