|
| 1 | +(in-package :lem-mcp-server) |
| 2 | + |
| 3 | +;;; Display/Screen Operation Tools |
| 4 | +;;; |
| 5 | +;;; Note on internal symbol usage: |
| 6 | +;;; This module requires access to internal lem-core symbols for complete |
| 7 | +;;; screen state capture. These are not exported because they are low-level |
| 8 | +;;; implementation details, but are necessary for AI agent integration: |
| 9 | +;;; - lem-core::get-string-and-attributes-at-point: Get line content with |
| 10 | +;;; syntax highlighting attributes (needed for accurate visual representation) |
| 11 | +;;; - lem-core::buffer-overlays: Access buffer overlay list |
| 12 | +;;; - lem-core::overlay-temporary-p: Check overlay lifetime |
| 13 | +;;; - lem-core::cursor-overlay, line-overlay, line-endings-overlay: Overlay types |
| 14 | +;;; Consider exporting these or creating a public API for editor introspection. |
| 15 | + |
| 16 | +;;; Helper functions |
| 17 | + |
| 18 | +(defun window-id (window) |
| 19 | + "Generate a unique ID for a window." |
| 20 | + (format nil "window-~A" (sxhash window))) |
| 21 | + |
| 22 | +(defun get-write-state (buffer) |
| 23 | + "Get the write state of a buffer." |
| 24 | + (cond ((buffer-read-only-p buffer) "readonly") |
| 25 | + ((buffer-modified-p buffer) "modified") |
| 26 | + (t "unmodified"))) |
| 27 | + |
| 28 | +(defun get-scroll-percent (window) |
| 29 | + "Get the scroll position as a percentage string." |
| 30 | + (let* ((buffer (window-buffer window)) |
| 31 | + (total-lines (buffer-nlines buffer)) |
| 32 | + (view-point (window-view-point window)) |
| 33 | + (top-line (line-number-at-point view-point)) |
| 34 | + (height (window-height window))) |
| 35 | + (cond ((<= total-lines height) "All") |
| 36 | + ((= top-line 1) "Top") |
| 37 | + ((>= (+ top-line height) total-lines) "Bot") |
| 38 | + (t (format nil "~D%" (round (* 100 (/ (1- top-line) (max 1 (- total-lines height)))))))))) |
| 39 | + |
| 40 | +(defun get-region-info (window) |
| 41 | + "Get selection/region information for a window." |
| 42 | + (let* ((buffer (window-buffer window)) |
| 43 | + (mark (buffer-mark buffer))) |
| 44 | + (if (and mark (mark-active-p buffer)) |
| 45 | + (let ((point (buffer-point buffer))) |
| 46 | + `(("active" . t) |
| 47 | + ("start" . (("line" . ,(line-number-at-point mark)) |
| 48 | + ("column" . ,(point-charpos mark)))) |
| 49 | + ("end" . (("line" . ,(line-number-at-point point)) |
| 50 | + ("column" . ,(point-charpos point)))))) |
| 51 | + `(("active" . :false))))) |
| 52 | + |
| 53 | +(defun color-to-string (color) |
| 54 | + "Convert a color to a string representation." |
| 55 | + (typecase color |
| 56 | + (null :null) |
| 57 | + (string color) |
| 58 | + (t (format nil "~A" color)))) |
| 59 | + |
| 60 | +(defun attribute-to-json (attr) |
| 61 | + "Convert a Lem attribute to JSON-serializable form." |
| 62 | + (if attr |
| 63 | + `(("foreground" . ,(color-to-string (attribute-foreground attr))) |
| 64 | + ("background" . ,(color-to-string (attribute-background attr))) |
| 65 | + ("bold" . ,(if (attribute-bold attr) t :false)) |
| 66 | + ("underline" . ,(if (attribute-underline attr) t :false)) |
| 67 | + ("reverse" . ,(if (attribute-reverse attr) t :false))) |
| 68 | + `(("foreground" . :null) |
| 69 | + ("background" . :null) |
| 70 | + ("bold" . :false) |
| 71 | + ("underline" . :false) |
| 72 | + ("reverse" . :false)))) |
| 73 | + |
| 74 | +(defun get-line-attributes (point) |
| 75 | + "Get attributes for a line at point. Returns list of (start end attribute) tuples." |
| 76 | + (handler-case |
| 77 | + (let ((attrs (lem-core::get-string-and-attributes-at-point point))) |
| 78 | + (cdr attrs)) |
| 79 | + (error () nil))) |
| 80 | + |
| 81 | +(defun get-visible-lines (window) |
| 82 | + "Get visible lines with their content and attributes." |
| 83 | + (let ((result '())) |
| 84 | + (with-point ((point (window-view-point window))) |
| 85 | + (loop :for y :from 0 :below (window-height window) |
| 86 | + :while (not (end-buffer-p point)) |
| 87 | + :do (let ((line-num (line-number-at-point point)) |
| 88 | + (text (line-string point)) |
| 89 | + (attrs (get-line-attributes point))) |
| 90 | + (push `(("line_number" . ,line-num) |
| 91 | + ("text" . ,text) |
| 92 | + ("wrapped" . :false) |
| 93 | + ("attributes" . ,(if attrs |
| 94 | + (mapcar (lambda (attr-spec) |
| 95 | + (destructuring-bind (start end attr &rest rest) attr-spec |
| 96 | + (declare (ignore rest)) |
| 97 | + `(("start" . ,start) |
| 98 | + ("end" . ,end) |
| 99 | + ("style" . ,(attribute-to-json attr))))) |
| 100 | + attrs) |
| 101 | + ()))) |
| 102 | + result)) |
| 103 | + (unless (line-offset point 1) |
| 104 | + (return)))) |
| 105 | + (nreverse result))) |
| 106 | + |
| 107 | +(defun get-modeline-info (window) |
| 108 | + "Get modeline information for a window." |
| 109 | + (when (window-use-modeline-p window) |
| 110 | + (let ((buffer (window-buffer window))) |
| 111 | + `(("buffer_name" . ,(buffer-name buffer)) |
| 112 | + ("write_state" . ,(get-write-state buffer)) |
| 113 | + ("major_mode" . ,(mode-name (buffer-major-mode buffer))) |
| 114 | + ("minor_modes" . ,(mapcar (lambda (mode) (mode-name mode)) |
| 115 | + (buffer-minor-modes buffer))) |
| 116 | + ("position" . ,(format nil "~D:~D" |
| 117 | + (line-number-at-point (buffer-point buffer)) |
| 118 | + (point-charpos (buffer-point buffer)))) |
| 119 | + ("scroll" . ,(get-scroll-percent window)))))) |
| 120 | + |
| 121 | +(defun get-window-info (window) |
| 122 | + "Get comprehensive information about a window." |
| 123 | + (let* ((buffer (window-buffer window)) |
| 124 | + (point (buffer-point buffer)) |
| 125 | + (is-current (eq window (current-window)))) |
| 126 | + `(("id" . ,(window-id window)) |
| 127 | + ("type" . ,(cond ((floating-window-p window) "floating") |
| 128 | + ((header-window-p window) "header") |
| 129 | + (t "normal"))) |
| 130 | + ("active" . ,(if is-current t :false)) |
| 131 | + ("position" . (("x" . ,(window-x window)) |
| 132 | + ("y" . ,(window-y window)))) |
| 133 | + ("size" . (("width" . ,(window-width window)) |
| 134 | + ("height" . ,(window-height window)))) |
| 135 | + ("buffer" . (("name" . ,(buffer-name buffer)) |
| 136 | + ("filename" . ,(or (buffer-filename buffer) :null)) |
| 137 | + ("modified" . ,(if (buffer-modified-p buffer) t :false)) |
| 138 | + ("readonly" . ,(if (buffer-read-only-p buffer) t :false)))) |
| 139 | + ("cursor" . (("line" . ,(line-number-at-point point)) |
| 140 | + ("column" . ,(point-charpos point)) |
| 141 | + ("screen_x" . ,(window-cursor-x window)) |
| 142 | + ("screen_y" . ,(window-cursor-y window)))) |
| 143 | + ("region" . ,(get-region-info window)) |
| 144 | + ("view" . (("top_line" . ,(line-number-at-point (window-view-point window))) |
| 145 | + ("visible_lines" . ,(window-height window)) |
| 146 | + ("scroll_percent" . ,(get-scroll-percent window)))) |
| 147 | + ("visible_content" . ,(get-visible-lines window)) |
| 148 | + ("modeline" . ,(or (get-modeline-info window) :null))))) |
| 149 | + |
| 150 | +(defun get-prompt-window-info () |
| 151 | + "Get prompt window (minibuffer equivalent) state." |
| 152 | + (let ((prompt-window (active-prompt-window))) |
| 153 | + (if prompt-window |
| 154 | + (let ((buffer (window-buffer prompt-window))) |
| 155 | + `(("active" . ,(if (eq prompt-window (current-window)) t :false)) |
| 156 | + ("prompt" . :null) |
| 157 | + ("input" . ,(buffer-text buffer)) |
| 158 | + ("message" . :null))) |
| 159 | + `(("active" . :false) |
| 160 | + ("prompt" . :null) |
| 161 | + ("input" . "") |
| 162 | + ("message" . :null))))) |
| 163 | + |
| 164 | +(defun get-overlay-type (overlay) |
| 165 | + "Determine the type of an overlay." |
| 166 | + (typecase overlay |
| 167 | + (lem-core::cursor-overlay "cursor") |
| 168 | + (lem-core::line-overlay "line") |
| 169 | + (lem-core::line-endings-overlay "line-endings") |
| 170 | + (t "highlight"))) |
| 171 | + |
| 172 | +(defun get-overlay-info (overlay) |
| 173 | + "Get information about a single overlay." |
| 174 | + (let ((start (overlay-start overlay)) |
| 175 | + (end (overlay-end overlay))) |
| 176 | + `(("buffer" . ,(buffer-name (overlay-buffer overlay))) |
| 177 | + ("type" . ,(get-overlay-type overlay)) |
| 178 | + ("start" . (("line" . ,(line-number-at-point start)) |
| 179 | + ("column" . ,(point-charpos start)))) |
| 180 | + ("end" . (("line" . ,(line-number-at-point end)) |
| 181 | + ("column" . ,(point-charpos end)))) |
| 182 | + ("attribute" . ,(attribute-to-json (overlay-attribute overlay))) |
| 183 | + ("temporary" . ,(if (lem-core::overlay-temporary-p overlay) t :false))))) |
| 184 | + |
| 185 | +(defun get-all-overlays () |
| 186 | + "Get all overlays from all buffers." |
| 187 | + (let ((result '())) |
| 188 | + (dolist (buffer (buffer-list)) |
| 189 | + (dolist (overlay (lem-core::buffer-overlays buffer)) |
| 190 | + (push (get-overlay-info overlay) result))) |
| 191 | + (nreverse result))) |
| 192 | + |
| 193 | +(defun get-header-windows-info () |
| 194 | + "Get information about header windows." |
| 195 | + (let ((result '())) |
| 196 | + (dolist (window (frame-header-windows (current-frame))) |
| 197 | + (push `(("id" . ,(window-id window)) |
| 198 | + ("position" . (("x" . ,(window-x window)) |
| 199 | + ("y" . ,(window-y window)))) |
| 200 | + ("size" . (("width" . ,(window-width window)) |
| 201 | + ("height" . ,(window-height window)))) |
| 202 | + ("content" . ,(buffer-text (window-buffer window)))) |
| 203 | + result)) |
| 204 | + (nreverse result))) |
| 205 | + |
| 206 | +;;; Main tool definition |
| 207 | + |
| 208 | +(define-mcp-tool "editor_get_screen" (include-floating-windows) |
| 209 | + (:description "Get current screen state including all windows, their content, cursor positions, overlays, and UI elements. Returns complete visual state of the editor." |
| 210 | + :input-schema (("type" . "object") |
| 211 | + ("properties" . (("include_floating_windows" . (("type" . "boolean") |
| 212 | + ("description" . "Include floating windows (popups, prompts). Default: true"))))) |
| 213 | + ("required" . ()))) |
| 214 | + (let* ((include-floating (if (null include-floating-windows) t include-floating-windows)) |
| 215 | + (all-windows (window-list)) |
| 216 | + (floating-windows (when include-floating |
| 217 | + (frame-floating-windows (current-frame)))) |
| 218 | + (prompt-window (active-prompt-window)) |
| 219 | + (windows-to-process (remove-if (lambda (w) |
| 220 | + (or (header-window-p w) |
| 221 | + (eq w prompt-window))) |
| 222 | + all-windows))) |
| 223 | + (with-output-to-string (s) |
| 224 | + (yason:encode |
| 225 | + (alist-to-hash-table |
| 226 | + `(("display" . (("width" . ,(display-width)) |
| 227 | + ("height" . ,(display-height)))) |
| 228 | + ("current_window_id" . ,(window-id (current-window))) |
| 229 | + ("windows" . ,(mapcar #'get-window-info windows-to-process)) |
| 230 | + ("floating_windows" . ,(when include-floating |
| 231 | + (mapcar #'get-window-info floating-windows))) |
| 232 | + ("header_windows" . ,(get-header-windows-info)) |
| 233 | + ("prompt" . ,(get-prompt-window-info)) |
| 234 | + ("overlays" . ,(get-all-overlays)))) |
| 235 | + s)))) |
0 commit comments