├── .gitignore ├── Eldev ├── cursorless-command-client.el ├── cursorless-hats.el ├── cursorless-log.el ├── cursorless-state.el ├── cursorless.el └── emacs_command_client.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.elc 2 | \#*\# 3 | 4 | # Added automatically by ‘eldev init’. 5 | /.eldev 6 | /Eldev-local 7 | -------------------------------------------------------------------------------- /Eldev: -------------------------------------------------------------------------------- 1 | ; -*- mode: emacs-lisp; lexical-binding: t -*- 2 | (eldev-use-package-archive 'melpa) 3 | 4 | (setq eldev-project-main-file "cursorless.el") 5 | 6 | (setq eldev-files-to-package '("cursorless.el" "cursorless-hats.el" "cursorless-state.el" "cursorless-command-client.el")) 7 | -------------------------------------------------------------------------------- /cursorless-command-client.el: -------------------------------------------------------------------------------- 1 | ;;; cursorless-command-client.el --- Description -*- lexical-binding: t; -*- 2 | ;;; Commentary: 3 | ;; Implements a command client for Emacs, forwarding cursorless commands over a 4 | ;; socket to the VSCode sidecar. 5 | ;; 6 | ;;; Code: 7 | 8 | (require 'command-server) 9 | 10 | (defconst command-server-directory-name "emacs-command-server" 11 | "Name of directory to use for the Emacs command server. Will be suffixed with the user's real UID.") 12 | 13 | (defvar cursorless--last-response-processed nil 14 | "Store the last time a response was processed. This is useful for ensuring multiple 15 | commands don't stomp on each other.") 16 | 17 | (defvar cursorless-running-command nil 18 | "Store whether a command is currently running to avoid certain actions (updating hats).") 19 | 20 | (defun cursorless--time-in-milliseconds () 21 | ;; TODO: there's probably a better way to do this. 22 | (string-to-number (format-time-string "%s.%3N"))) 23 | 24 | (defun cursorless-command-server-directory () 25 | ;; TODO: on windows suffix should be empty, I think, assuming that 26 | ;; (temporary-file-directory) returns the appropriate user-specific temp dir 27 | ;; on windows. 28 | (let ((suffix (format "-%s" (user-real-uid)))) 29 | (expand-file-name 30 | (concat command-server-directory-name suffix) 31 | (temporary-file-directory)))) 32 | 33 | (defun cursorless-command-server-start () 34 | (interactive) 35 | (let ((d (cursorless-command-server-directory))) 36 | (unless (and (file-exists-p d) (file-directory-p d)) 37 | (make-directory d)))) 38 | 39 | (defun cursorless-command-server-quit () 40 | (interactive) 41 | (delete-directory (cursorless-command-server-directory) t)) 42 | 43 | (cursorless-command-server-start) 44 | (add-hook 'kill-emacs-hook 'cursorless-command-server-quit) 45 | 46 | 47 | (defvar cursorless--current-command-uuid nil) 48 | 49 | (defun cursorless--command-server-handler (command-uuid wait-for-finish args) 50 | ;; TODO: we ignore wait-for-finish 51 | (setq cursorless-running-command t) 52 | (setq cursorless--current-command-uuid command-uuid) 53 | (let ((payload (make-hash-table :size 2))) 54 | (puthash "command" "cursorless" payload) 55 | (puthash "cursorlessArgs" (json-serialize args) payload) 56 | (cursorless-log (format "sending command: %s" (cursorless--json-pretty-print (json-encode payload)))) 57 | (setq payload (json-serialize payload)) 58 | (cursorless-send payload))) 59 | 60 | (add-to-list 'command-server-command-handlers 61 | '("cursorless.command" . cursorless--command-server-handler)) 62 | 63 | ;;; ---------- emacs -> vscode over cursorless socket ---------- 64 | (defvar cursorless-socket-buffer (generate-new-buffer "*cursorless-vscode-socket*")) 65 | 66 | 67 | (defun cursorless-sentinel (proc event) 68 | (let ((status (process-status proc))) 69 | (if (not (and (equal status 'closed) 70 | (equal event "connection broken by remote peer\n"))) 71 | (warn "Cursorless: unexpected error on communicating with vscode: %s, %s" status event) 72 | (cursorless-receive (with-current-buffer cursorless-socket-buffer 73 | (goto-char (point-min)) ;; json-parse-buffer parses forward from point. 74 | (json-parse-buffer)))))) 75 | 76 | (defun cursorless-send (cmd) 77 | (with-current-buffer cursorless-socket-buffer 78 | (erase-buffer)) 79 | (let ((p (make-network-process 80 | :name "cursorless" 81 | :family 'local 82 | :remote (expand-file-name (concat cursorless-directory "/vscode-socket")) 83 | :buffer cursorless-socket-buffer 84 | :sentinel 'cursorless-sentinel))) 85 | ;; send the command 350ms after the last command was processed (or now). 86 | ;; this adds a bit of latency to chaining commands, but they work. 87 | (run-at-time (if cursorless--last-response-processed 88 | (+ (- cursorless--last-response-processed (cursorless--time-in-milliseconds)) .35)) nil 'process-send-string p cmd))) 89 | 90 | (defun cursorless--apply-selections (selections) 91 | (when selections 92 | ;; assume 1 cursor for now. 93 | (let* ((cursor (elt selections 0)) 94 | (active (gethash "active" cursor)) 95 | (anchor (gethash "anchor" cursor)) 96 | (line (gethash "line" active)) 97 | (column (gethash "character" active)) 98 | (anchor-line (gethash "line" anchor)) 99 | (anchor-column (gethash "character" anchor)) 100 | (no-selection (and (eql line anchor-line) (eql column anchor-column)))) 101 | ;; Update the selection. 102 | (unless no-selection 103 | (goto-char (point-min)) 104 | (forward-line anchor-line) 105 | (forward-char anchor-column) 106 | ;; location = (point), nomsg = t 107 | (push-mark (point) t)) 108 | ;; Update cursor position. 109 | (cursorless-goto-line-column line column) 110 | (if no-selection (deactivate-mark) 111 | (activate-mark t) 112 | (setq-local transient-mark-mode (cons 'only transient-mark-mode)))))) 113 | 114 | (defun cursorless--get-buffer-from-temporary-file (temporary-file) 115 | (seq-find (lambda (buffer) 116 | (with-current-buffer buffer 117 | (and (local-variable-p 'cursorless-temporary-file) 118 | (string-equal temporary-file cursorless-temporary-file)))) (buffer-list))) 119 | 120 | (defun cursorless-receive (response) 121 | ;; TODO: handle replies like "pong" which don't give a new state. 122 | 123 | ;; TODO: The command finished, process its results. We should (a) propagate 124 | ;; results back across the command server to talon; (b) apply changes using 125 | ;; the "newState" field. 126 | ;; 127 | ;; To apply changes: 128 | ;; - figure out which buffer to update from "path" 129 | ;; - diff the "contentsPath" against buffer (or temporary file?) contents & apply updates 130 | ;; - update the cursor(s) from "cursors" 131 | (cursorless-log (format "receiving response: %s" (cursorless--json-pretty-print (json-encode response)))) 132 | (if-let ((command-exception (gethash "commandException" response))) 133 | (progn 134 | (message command-exception) 135 | (setq cursorless-running-command nil)) 136 | (let* ((new-state (gethash "newState" response)) 137 | (path (gethash "path" new-state)) 138 | (contents-path (gethash "contentsPath" new-state)) 139 | ;; Find the buffer to update. 140 | (buffer-to-update (cursorless--get-buffer-from-temporary-file path))) 141 | (if (not buffer-to-update) 142 | (error "Couldn't find buffer to update, ignoring!")) 143 | (with-current-buffer buffer-to-update 144 | ;; Ideally we'd do a diff and then apply the minimal update. Instead I'm 145 | ;; just going to replace the whole buffer. 146 | (unless (file-exists-p contents-path) (error "No contents file!")) 147 | (let ((coding-system-for-read 'utf-8) 148 | (file-name-handler-alist '())) 149 | (insert-file-contents contents-path nil nil nil t)) 150 | ;; Update cursor & selection. 151 | (cursorless--apply-selections (gethash "cursors" new-state)) 152 | (cursorless-log (format "applied response")) 153 | (setq cursorless-running-command nil) 154 | ;; This keeps various things up-to-date, eg. hl-line-mode. 155 | ;; This also runs our send-state function. 156 | (command-server--write-response cursorless--current-command-uuid) 157 | (run-hooks 'post-command-hook) 158 | (setq cursorless--last-response-processed (cursorless--time-in-milliseconds)))))) 159 | 160 | 161 | (provide 'cursorless-command-client) 162 | ;;; cursorless-command-client.el ends here 163 | -------------------------------------------------------------------------------- /cursorless-hats.el: -------------------------------------------------------------------------------- 1 | ;;; cursorless-hats.el --- Description -*- lexical-binding: t; -*- 2 | ;;; Commentary: 3 | ;; The code for managing hat decorations. 4 | ;; 5 | ;;; Code: 6 | 7 | (require 'json) 8 | 9 | ;; READING & DRAWING HATS FROM CURSORLESS 10 | (defconst cursorless-hats-file 11 | (concat cursorless-directory "vscode-hats.json")) 12 | 13 | (defcustom cursorless-color-alist 14 | '((default . "#999") 15 | (blue . "#04f") 16 | (red . "#e00") 17 | (pink . "#ffa0ff") 18 | (green . "#0b0") 19 | (yellow . "ffc000") 20 | (userColor1 . "#6a00ff") 21 | (userColor2 . "#ffd8b1")) 22 | "The mapping from cursorless color phrases to emacs colors." 23 | :type '(alist :key-type symbol :value-type string) 24 | :group 'cursorless) 25 | 26 | (defvar cursorless-show-hats t) 27 | 28 | (defvar cursorless-updating-hats nil) 29 | (defvar cursorless-hats-update-timer nil) 30 | (defun cursorless-hats-update-callback (&optional event) 31 | (unless cursorless-running-command 32 | ;; recursive invocations can occur if running cursorless-update-hats causes 33 | ;; the hats file to change; eg. if cursorless-update-hats writes to *Messages* 34 | ;; and we're visiting *Messages*. Without care this can lock up emacs. 35 | (if cursorless-updating-hats 36 | (progn 37 | (cursorless-log "cursorless-hats-update-callback: recursive invocation detected!") 38 | (warn "cursorless-hats-update-callback: recursive invocation detected!")) 39 | ;; the hats file could be stale which would make finding the right cursor positions fail 40 | ;; with an end-of-buffer error. ignore these errors since they're normally remediated 41 | ;; very quickly. 42 | (if (not (ignore-error end-of-buffer 43 | (setq cursorless-updating-hats t) 44 | (when cursorless-show-hats 45 | (let ((hats-json (cursorless-read-hats-json))) 46 | (seq-each (lambda (file-hats) 47 | (let* ((file (car file-hats)) 48 | (hats (cdr file-hats)) 49 | (file (symbol-name file)) 50 | (related-buffer (gethash file cursorless-temporary-file-buffers))) 51 | (if (not related-buffer) 52 | (message "temporary file not associated with a buffer: %S" file) 53 | (cursorless-update-hats related-buffer hats)))) hats-json))) 54 | 55 | t)) 56 | (cursorless-log "failed to update hats (end-of-buffer aka stale hats file)")) 57 | (setq cursorless-updating-hats nil)))) 58 | 59 | (defun cursorless-hats-change-callback (event) 60 | (when (and (equal (nth 1 event) 'changed) 61 | (string-equal (nth 2 event) (expand-file-name cursorless-hats-file))) 62 | (when cursorless-updating-hats 63 | (cursorless-log (format "cursorless-hats CHANGE RECURSIVE, set cursorless-updating-hats to nil to re-enable\n%s" (backtrace-to-string))) 64 | (message "cursorless-hats CHANGE RECURSIVE, set cursorless-updating-hats to nil to re-enable")) 65 | (unless cursorless-updating-hats 66 | (cursorless-hats-update-callback)))) 67 | 68 | (defvar cursorless-hats-watcher 69 | (progn 70 | (when (and (boundp 'cursorless-hats-watcher) cursorless-hats-watcher) 71 | (file-notify-rm-watch cursorless-hats-watcher)) 72 | ;; We have to watch the whole directory for changes otherwise we can't get notifications about 73 | ;; the hats file being created on macOS. 74 | (file-notify-add-watch cursorless-directory '(change) 'cursorless-hats-change-callback))) 75 | 76 | ;; FIXME: need to initialize hats whenever we switch to a buffer without them. 77 | (defun cursorless-show-hats () 78 | (interactive) 79 | (when cursorless-show-hats (cursorless-clear-overlays)) 80 | (setq cursorless-show-hats t) 81 | (cursorless-hats-update-callback)) 82 | 83 | (defun cursorless-hide-hats () 84 | (interactive) 85 | ;; TODO: filter buffer-list by a cursorless-mode marker 86 | (seq-each (lambda (buffer) 87 | (with-current-buffer buffer 88 | (cursorless-clear-overlays))) (buffer-list)) 89 | (setq cursorless-show-hats nil)) 90 | 91 | (defun cursorless-clear-overlays () 92 | (interactive) 93 | (remove-overlays nil nil 'cursorless t)) 94 | 95 | (defun cursorless-read-hats-json () 96 | "Read the hats file and return an alist. 97 | 98 | Hats are stored as a JSON object like: 99 | { 100 | FILEPATH: { 101 | COLORNAME: [ { \"start\": { \"line\": l, \"character\": c }, 102 | \"end\": { \"line\": l, \"character\": c } }, 103 | ... more hat positions ... ] 104 | ... more colors ... 105 | }, 106 | ... more files ... 107 | } 108 | 109 | COLORNAME is sometimes a color name e.g. blue and sometimes a color followed 110 | by a shape e.g. blue-bolt." 111 | (with-temp-buffer 112 | (insert-file-contents-literally cursorless-hats-file) 113 | (json-parse-buffer :object-type 'alist))) 114 | 115 | (defun cursorless-update-hats (buffer hats) 116 | "Update BUFFER with HATS." 117 | (with-current-buffer buffer 118 | (cursorless-log (format "updating hats on %S" buffer)) 119 | (cursorless-clear-overlays) 120 | (seq-map (lambda(color-shape-positions) 121 | (let* ((color-shape (string-split (symbol-name (car color-shape-positions)) "-")) 122 | (color (car color-shape)) 123 | (shape (cadr color-shape)) 124 | (draw-hat (apply-partially 'cursorless-draw-hat (intern color) shape))) 125 | (seq-map draw-hat (cdr color-shape-positions)))) hats) 126 | (cursorless-log (format "done updating hats on %S" buffer)))) 127 | 128 | (defun cursorless-point-from-cursorless-position (cursorless-position) 129 | "Return the proper point for an alist from a cursorless position. 130 | 131 | CURSORLESS-POSITION is an alist parsed from `cursorless-read-hats-json'." 132 | (let ((pos (alist-get 'start cursorless-position))) 133 | (save-excursion 134 | (goto-char (point-min)) 135 | ;; consider using https://emacs.stackexchange.com/a/3822. this offers roughly a 10x speedup. 136 | (forward-line (alist-get 'line pos)) 137 | (forward-char (alist-get 'character pos)) 138 | (point)))) 139 | 140 | (defun cursorless--get-hat-color (cursorless-color) 141 | (if-let ((hat-color (alist-get cursorless-color cursorless-color-alist))) 142 | hat-color 143 | (display-warning 'cursorless 144 | (format "Unable to find mapping for cursorless color %s." 145 | cursorless-color) :error))) 146 | 147 | (defun cursorless-draw-hat (cursorless-color cursorless-shape cursorless-position) 148 | "Draw an individual hat on the current buffer. 149 | 150 | CURSORLESS-COLOR is a color name (e.g. default, blue, pink) that gets translated 151 | through `cursorless-color-alist'. 152 | 153 | CURSORLESS-SHAPE is the shape to render. If CURSORLESS-SHAPE is nil, the default 154 | dot gets rendered. 155 | 156 | CURSORLESS-POSITION is an alist parsed from `cursorless-read-hats-json'." 157 | (when-let* ((hat-color (cursorless--get-hat-color cursorless-color)) 158 | (hat-point (cursorless-point-from-cursorless-position cursorless-position)) 159 | (hat-overlay (make-overlay hat-point (+ hat-point 1)))) 160 | (overlay-put hat-overlay 'cursorless t) 161 | (cond ((null cursorless-shape) 162 | (overlay-put hat-overlay 'face `(:cursorless ,hat-color))) 163 | ((string-equal cursorless-shape "frame") 164 | (overlay-put hat-overlay 'face `(:box (:line-width (0 . -2) :color ,hat-color)))) 165 | (t (display-warning 166 | 'cursorless 167 | (format "Unable to find mapping for cursorless shape %s." cursorless-shape) :error))))) 168 | 169 | (provide 'cursorless-hats) 170 | ;;; cursorless-hats.el ends here 171 | -------------------------------------------------------------------------------- /cursorless-log.el: -------------------------------------------------------------------------------- 1 | ;;; cursorless-log.el --- Description -*- lexical-binding: t; -*- 2 | ;;; Commentary: 3 | ;; Description 4 | ;; 5 | ;;; Code: 6 | 7 | (require 'json) 8 | 9 | (defconst cursorless-log-buffer "*cursorless-log*") 10 | 11 | (defun cursorless--json-pretty-print (s) 12 | (with-temp-buffer 13 | (insert s) 14 | (json-pretty-print-buffer) 15 | (buffer-string))) 16 | 17 | (defun cursorless--truncate-log-buffer () 18 | (interactive) 19 | (save-excursion 20 | (with-current-buffer (get-buffer-create cursorless-log-buffer) 21 | (goto-char (point-max)) 22 | (forward-line (- 5000)) 23 | (beginning-of-line) 24 | (let ((inhibit-read-only t)) 25 | (delete-region (point-min) (point)))))) 26 | 27 | (defun cursorless-log (message) 28 | (cursorless--truncate-log-buffer) 29 | (let ((buffer-logged-from (current-buffer)) 30 | (line-col (cursorless-line-and-column (point)))) 31 | (with-current-buffer (get-buffer-create cursorless-log-buffer) 32 | (goto-char (point-max)) 33 | (insert (make-string 70 ?=) "\n" 34 | (format-time-string "%s.%3N") "\n" 35 | (format " current-buffer: %S" buffer-logged-from) "\n" 36 | (format " line/col: %S" line-col) "\n" 37 | (string-join (seq-map (lambda(value) 38 | (format " %20s: %S" value (symbol-value value))) 39 | '(cursorless-running-command cursorless-updating-hats cursorless--last-response-processed)) "\n") "\n\n" 40 | " " message "\n") 41 | (goto-char (point-max)) 42 | (let ((windows (get-buffer-window-list (current-buffer) nil t))) 43 | (while windows 44 | (set-window-point (car windows) (point-max)) 45 | (setq windows (cdr windows))))))) 46 | 47 | (provide 'cursorless-log) 48 | ;;; cursorless-log.el ends here 49 | -------------------------------------------------------------------------------- /cursorless-state.el: -------------------------------------------------------------------------------- 1 | ;;; cursorless-state.el.el --- Description -*- lexical-binding: t; -*- 2 | ;;; Commentary: 3 | ;; The code for syncing editor state with vscode. 4 | ;; 5 | ;;; Code: 6 | 7 | (defvar cursorless-serial-number 0) 8 | (defconst cursorless-editor-state-file 9 | (concat cursorless-directory "editor-state.json")) 10 | 11 | ;; Maps from temporary file paths (strings) to their buffers. 12 | (defvar cursorless-temporary-file-buffers (make-hash-table :test 'equal)) 13 | (make-variable-buffer-local 'cursorless-temporary-file) 14 | ;; permanent-local --> survives major mode change 15 | (put 'cursorless-temporary-file 'permanent-local t) 16 | 17 | ;; Call if cursorless-temporary-file-buffers gets out of sync. Shouldn't happen 18 | ;; in normal use. 19 | (defun cursorless-refresh-temporary-file-buffers () 20 | (interactive) 21 | (clrhash cursorless-temporary-file-buffers) 22 | (dolist (b (buffer-list)) 23 | (with-current-buffer b 24 | (when (and (local-variable-p 'cursorless-temporary-file)) 25 | (puthash cursorless-temporary-file b cursorless-temporary-file-buffers))))) 26 | 27 | (defun cursorless-kill-buffer-callback () 28 | (when (local-variable-p 'cursorless-temporary-file) 29 | (remhash cursorless-temporary-file cursorless-temporary-file-buffers) 30 | (delete-file cursorless-temporary-file))) 31 | 32 | ;; need buffer-list-update-hook or kill-buffer-hook 33 | (add-hook 'kill-buffer-hook 'cursorless-kill-buffer-callback) 34 | 35 | (defvar cursorless-sync-state t) 36 | 37 | ;; TODO: this doesn't work for comint buffers, which can update without the user 38 | ;; issuing a command. Use after-change-functions instead/in addition? 39 | (add-hook 'post-command-hook 'cursorless--send-state-when-idle) 40 | ;; TODO: address the issue of vscode autorevert changing cursor positions. 41 | (defvar cursorless-send-state-timer (run-with-idle-timer .2 t 'cursorless-send-state)) 42 | 43 | ;; TODO: do we really need cursorless-{enable,disable}-sync? 44 | (defun cursorless-enable-sync () 45 | (interactive) 46 | (setq cursorless-sync-state t) 47 | (cursorless-send-state)) 48 | 49 | (defun cursorless-disable-sync () 50 | (interactive) 51 | (setq cursorless-sync-state nil)) 52 | 53 | ;;; Scrolling seems janky, but it doesn't look like we're causing it? 54 | ;;; that is, removing this hook doesn't seem to fix the issue. 55 | (defun cursorless--send-state-when-idle () 56 | (run-with-idle-timer 0 nil 'cursorless-send-state)) 57 | 58 | (defun cursorless--should-draw-hats-p () 59 | (and (not (minibufferp)) 60 | (not (string-equal (buffer-name) "*cursorless-log*")) 61 | ;; TODO: warn about not drawing hats on huge buffers 62 | (< (buffer-size) 5000000))) 63 | 64 | (defun cursorless-send-state () 65 | ;; TODO: maybe figure out how to avoid dumping state if it didn't change? 66 | ;; but when will that happen? 67 | (when (and cursorless-sync-state 68 | (cursorless--should-draw-hats-p) 69 | (not cursorless-running-command)) 70 | (setq cursorless-serial-number (+ 1 cursorless-serial-number)) 71 | (cursorless-dump-state))) 72 | 73 | (defun cursorless-dump-state () 74 | (interactive) 75 | ;; TODO: only write if buffer contents have changed since last write! 76 | ;; Use utf-8 and avoid auto-compression etc based on file extension. 77 | (let ((coding-system-for-write 'utf-8) 78 | (file-name-handler-alist '())) 79 | (write-region (point-min) (point-max) (cursorless-temporary-file-path) nil 'ignore-message)) 80 | (let ((state (cursorless-get-state)) 81 | (temp-file (make-temp-file "emacs-editor-state-"))) 82 | (cursorless-log (format "dumping state for %S \n %s" (current-buffer) (cursorless--json-pretty-print (json-encode state)))) 83 | (with-temp-buffer 84 | (json-insert state) 85 | (write-region (point-min) (point-max) temp-file nil 'ignore-message)) 86 | (rename-file temp-file cursorless-editor-state-file t))) 87 | 88 | ;; Serialize editor state to file, at the moment: 89 | ;; - a serial number 90 | ;; - current file path 91 | ;; - top & bottom visible lines 92 | ;; edge case: long lines without wrapping, does cursorless hat them? 93 | ;; looks like it does. 94 | ;; - where the cursors/selections are 95 | (defun cursorless-get-state () 96 | ;; produces something that can be passed to json-serialize 97 | ;; in this case, a plist 98 | (list 99 | :serialNumber cursorless-serial-number 100 | :activeEditor 101 | (list 102 | :path (or (buffer-file-name) :null) 103 | :temporaryFilePath (cursorless-temporary-file-path) 104 | :firstVisibleLine (1- (line-number-at-pos (window-start))) 105 | :lastVisibleLine (line-number-at-pos (window-end)) 106 | ;; where the cursors are. in emacs, only one cursor, so a singleton vector. 107 | ;; note that cursorless wants line/column, not offset. 108 | ;; TODO: if transient-mark-mode is enabled, represent the whole selection. 109 | :cursors (vector (cursorless-line-and-column (point)))))) 110 | 111 | (defun cursorless-temporary-file-path (&optional given-extension) 112 | "Allows passing in an already known extension, which is useful for testing." 113 | (unless (and (local-variable-p 'cursorless-temporary-file) 114 | ;; If file has been deleted we must make a new one. 115 | (file-exists-p cursorless-temporary-file)) 116 | (let* ((file-extension (or (and (buffer-file-name) 117 | (file-name-extension (buffer-file-name))) given-extension)) 118 | (suffix (if file-extension (concat "." file-extension) "")) 119 | (dirname (concat (file-name-as-directory temporary-file-directory) 120 | "cursorless.el/")) 121 | ;; TODO: what is this regex for? 122 | (name (replace-regexp-in-string "[*/\\\\]" "_" (buffer-name))) 123 | (prefix (concat dirname name "-"))) 124 | (make-directory dirname t) 125 | ;; make-temp-file-internal because it doesn't try to do magic with file names 126 | (setq cursorless-temporary-file (make-temp-file-internal prefix nil suffix nil)) 127 | (puthash cursorless-temporary-file (current-buffer) 128 | cursorless-temporary-file-buffers))) 129 | cursorless-temporary-file) 130 | 131 | (defun cursorless--get-window-state (window) 132 | (let ((current-window (selected-window))) 133 | (with-selected-window window 134 | (if (not (window-parameter window 'cursorless-id)) 135 | (set-window-parameter window 'cursorless-id (int-to-string (abs (random))))) 136 | (list 137 | :id (window-parameter window 'cursorless-id) 138 | :active (or (eq window current-window) :false) 139 | :path (or (buffer-file-name) :null) 140 | :temporaryFilePath (cursorless-temporary-file-path) 141 | :firstVisibleLine (line-number-at-pos (window-start)) 142 | ;; window-end needs update t for some reason... some times. maybe it's calling the redraw 143 | ;; command from the minibuffer, that would make the window smaller right 144 | ;; before getting the window-end. 145 | :lastVisibleLine (line-number-at-pos (window-end window t)) 146 | ;; where the cursors are. in emacs, only one cursor, so a singleton vector. 147 | ;; note that cursorless wants line/column, not offset. 148 | ;; TODO: if transient-mark-mode is enabled, represent the whole selection. 149 | :cursors (vector (cursorless-line-and-column (point))))))) 150 | 151 | 152 | (provide 'cursorless-state) 153 | ;;; cursorless-state.el ends here 154 | -------------------------------------------------------------------------------- /cursorless.el: -------------------------------------------------------------------------------- 1 | ;;; cursorless.el --- Voice based structural editing with Cursorless -*- lexical-binding: t; -*- 2 | ;; 3 | ;; Version: 0.0.1 4 | ;; Package-Requires: ((emacs "28.1") (command-server "0.0.1")) 5 | ;; Keywords: cursorless, voice 6 | ;; Homepage: https://github.com/cursorless-everywhere/emacs-cursorless 7 | ;; 8 | ;; This file is not part of GNU Emacs. 9 | ;; 10 | ;;; Code: 11 | 12 | (require 'seq) 13 | (require 'svg) 14 | (require 'filenotify) 15 | 16 | (defconst cursorless-directory "~/.cursorless/") 17 | 18 | (defun cursorless-line-and-column (pos) 19 | (list 20 | ;; I thought cursorless wanted 1-indexed line #s, but 0-indexed seems to make 21 | ;; it work properly? 22 | 'line (1- (line-number-at-pos pos nil)) 23 | ;; (current-column) would be wrong here: we want # of characters since start 24 | ;; of line, not the logical position. (eg. tab counts as 1 char.) 25 | 'column (save-excursion 26 | (goto-char pos) 27 | (- pos (line-beginning-position))))) 28 | 29 | (defun cursorless-goto-line-column (line column) 30 | (goto-char (point-min)) 31 | (forward-line line) 32 | ;; TODO: check we haven't gone over the end of the line 33 | (forward-char column)) 34 | 35 | (defun line-and-column-to-offset (line column) 36 | (save-mark-and-excursion 37 | (save-restriction 38 | (widen) 39 | (cursorless-goto-line-column line column) 40 | (point)))) 41 | 42 | ;; Load everything. 43 | (let ((load-path (cons (file-name-directory (or load-file-name (buffer-file-name))) 44 | load-path))) 45 | (require 'cursorless-log) 46 | (require 'cursorless-command-client) 47 | (require 'cursorless-state) 48 | (require 'cursorless-hats)) 49 | 50 | (provide 'cursorless) 51 | ;;; cursorless.el ends here 52 | -------------------------------------------------------------------------------- /emacs_command_client.py: -------------------------------------------------------------------------------- 1 | from talon import Context, actions 2 | 3 | ctx = Context() 4 | 5 | ctx.matches = r""" 6 | app: emacs 7 | app: Emacs 8 | """ 9 | 10 | ctx.tags = ["user.command_client"] 11 | 12 | 13 | @ctx.action_class("user") 14 | class UserActions: 15 | def command_server_directory() -> str: 16 | return "emacs-command-server" 17 | 18 | def trigger_command_server_command_execution(): 19 | # PROBLEM: pressing ctrl-f17 in isearch-mode cancels it :( 20 | actions.key("ctrl-f17") 21 | --------------------------------------------------------------------------------