|
19 | 19 | (require 'flycheck nil t) |
20 | 20 | (require 'ht nil t) |
21 | 21 |
|
| 22 | +(require 'eca-api) |
22 | 23 | (require 'eca-util) |
23 | 24 |
|
24 | 25 | (declare-function lsp-diagnostics "lsp-mode" (current-workspace?)) |
25 | 26 | (declare-function lsp-get "lsp-mode" (from key)) |
| 27 | +(declare-function lsp "lsp-mode" (&optional arg)) |
| 28 | +(declare-function lsp-request-async "lsp-mode" (method params callback &rest plist)) |
| 29 | +(declare-function lsp-workspaces "lsp-mode" ()) |
| 30 | +(declare-function lsp-session "lsp-mode" ()) |
| 31 | +(declare-function lsp-session-folders "lsp-mode" (session)) |
| 32 | +(declare-function lsp--workspace-server-capabilities "lsp-mode" (workspace)) |
| 33 | +(declare-function lsp--find-clients "lsp-mode" ()) |
26 | 34 | (declare-function ht-select "ht" (function table)) |
27 | 35 | (declare-function flymake-diagnostic-code "flymake" (diagnostic)) |
28 | 36 | (declare-function flycheck-running-p "flycheck" ()) |
@@ -261,5 +269,192 @@ If URI is non-nil, filter to that uri." |
261 | 269 | (vconcat (eca-editor--collect-all-diagnostics |
262 | 270 | uri workspaces))))) |
263 | 271 |
|
| 272 | +(defun eca-editor--lsp-nav-available-p () |
| 273 | + "Return non-nil when lsp-mode is available for navigation requests." |
| 274 | + (featurep 'lsp-mode)) |
| 275 | + |
| 276 | +(defun eca-editor--lsp-buffer-workspaces () |
| 277 | + "Return the lsp-mode workspaces associated with the current buffer." |
| 278 | + (and (fboundp 'lsp-workspaces) (lsp-workspaces))) |
| 279 | + |
| 280 | +(defun eca-editor--lsp-workspaces-ready-p (workspaces) |
| 281 | + "Return non-nil when some of WORKSPACES finished initializing. |
| 282 | +A lsp-mode workspace only gets its server capabilities after the |
| 283 | +initialize handshake, so none having them means the server is |
| 284 | +still starting. Fails open: workspaces that cannot be inspected |
| 285 | +count as ready so the request is still attempted." |
| 286 | + (seq-some (lambda (workspace) |
| 287 | + (condition-case nil |
| 288 | + (and (lsp--workspace-server-capabilities workspace) t) |
| 289 | + (error t))) |
| 290 | + workspaces)) |
| 291 | + |
| 292 | +(defun eca-editor--lsp-known-root-p (path) |
| 293 | + "Return non-nil if PATH is inside a folder known to the lsp-mode session." |
| 294 | + (and (fboundp 'lsp-session) |
| 295 | + (fboundp 'lsp-session-folders) |
| 296 | + (seq-some (lambda (folder) |
| 297 | + (f-ancestor-of? folder path)) |
| 298 | + (lsp-session-folders (lsp-session))))) |
| 299 | + |
| 300 | +(defun eca-editor--lsp-start () |
| 301 | + "Start lsp-mode in the current buffer when an installed client matches. |
| 302 | +Return non-nil when a start was attempted, nil when no installed |
| 303 | +lsp-mode client supports this buffer. Checking the clients first |
| 304 | +avoids lsp-mode's interactive server-install prompt, which must |
| 305 | +never fire from the process filter." |
| 306 | + (when (or (not (fboundp 'lsp--find-clients)) |
| 307 | + (lsp--find-clients)) |
| 308 | + (lsp) |
| 309 | + t)) |
| 310 | + |
| 311 | +(defun eca-editor--lsp-request-async (method params callback error-callback) |
| 312 | + "Send async lsp-mode request METHOD with PARAMS. |
| 313 | +Call CALLBACK with the result or ERROR-CALLBACK with the error." |
| 314 | + (lsp-request-async method params callback |
| 315 | + :error-handler error-callback |
| 316 | + :mode 'detached)) |
| 317 | + |
| 318 | +(defun eca-editor--file-buffer (path) |
| 319 | + "Return a buffer visiting PATH, opening it in background if needed." |
| 320 | + (or (find-buffer-visiting path) |
| 321 | + (let ((enable-local-variables :safe) |
| 322 | + (large-file-warning-threshold nil)) |
| 323 | + (find-file-noselect path)))) |
| 324 | + |
| 325 | +(defun eca-editor--lsp-obj-get (obj key) |
| 326 | + "Get keyword KEY from lsp result OBJ. |
| 327 | +Supports both lsp-mode result representations: hash-tables with |
| 328 | +string keys and plists with keyword keys." |
| 329 | + (cond |
| 330 | + ((hash-table-p obj) (gethash (substring (symbol-name key) 1) obj)) |
| 331 | + ((listp obj) (plist-get obj key)))) |
| 332 | + |
| 333 | +(defun eca-editor--lsp-position->eca (position) |
| 334 | + "Convert 0-based lsp POSITION to a 1-based eca position." |
| 335 | + (list :line (1+ (or (eca-editor--lsp-obj-get position :line) 0)) |
| 336 | + :character (1+ (or (eca-editor--lsp-obj-get position :character) 0)))) |
| 337 | + |
| 338 | +(defun eca-editor--lsp-location->eca (location) |
| 339 | + "Convert a lsp Location or LocationLink LOCATION to an eca location." |
| 340 | + (when location |
| 341 | + (let ((uri (or (eca-editor--lsp-obj-get location :uri) |
| 342 | + (eca-editor--lsp-obj-get location :targetUri))) |
| 343 | + (range (or (eca-editor--lsp-obj-get location :range) |
| 344 | + (eca-editor--lsp-obj-get location :targetSelectionRange) |
| 345 | + (eca-editor--lsp-obj-get location :targetRange)))) |
| 346 | + (when (and uri range) |
| 347 | + (list :uri uri |
| 348 | + :range (list :start (eca-editor--lsp-position->eca |
| 349 | + (eca-editor--lsp-obj-get range :start)) |
| 350 | + :end (eca-editor--lsp-position->eca |
| 351 | + (eca-editor--lsp-obj-get range :end)))))))) |
| 352 | + |
| 353 | +(defun eca-editor--lsp-locations->eca (result) |
| 354 | + "Normalize a lsp navigation RESULT into a vector of eca locations. |
| 355 | +RESULT may be nil, a single Location, or a sequence of Location |
| 356 | +or LocationLink." |
| 357 | + (let ((locations (cond |
| 358 | + ((null result) nil) |
| 359 | + ((vectorp result) (append result nil)) |
| 360 | + ((hash-table-p result) (list result)) |
| 361 | + ((keywordp (car-safe result)) (list result)) |
| 362 | + (t result)))) |
| 363 | + (vconcat (delq nil (mapcar #'eca-editor--lsp-location->eca locations))))) |
| 364 | + |
| 365 | +(defun eca-editor--lsp-error-message (err) |
| 366 | + "Extract a human readable message from the lsp-mode error ERR." |
| 367 | + (cond |
| 368 | + ((null err) "lsp-mode request failed") |
| 369 | + ((stringp err) err) |
| 370 | + ((or (hash-table-p err) (keywordp (car-safe err))) |
| 371 | + (or (eca-editor--lsp-obj-get err :message) (format "%s" err))) |
| 372 | + (t (format "%s" err)))) |
| 373 | + |
| 374 | +(defun eca-editor--nav-request (session request params method extra-params) |
| 375 | + "Ask lsp-mode for the METHOD locations of the symbol in PARAMS. |
| 376 | +Respond to the server REQUEST of SESSION following the status |
| 377 | +contract of the `editor/getDefinition' and `editor/getReferences' |
| 378 | +requests: success, starting, no-server or error. EXTRA-PARAMS |
| 379 | +are appended to the lsp request params. Return the response |
| 380 | +plist for sync answers or `:async' when the lsp request was fired |
| 381 | +and the response will be sent later." |
| 382 | + (let* ((uri (plist-get params :uri)) |
| 383 | + (position (plist-get params :position)) |
| 384 | + (path (and uri (eca--uri-to-path uri)))) |
| 385 | + (cond |
| 386 | + ((not (eca-editor--lsp-nav-available-p)) |
| 387 | + (list :status "no-server" |
| 388 | + :message "lsp-mode is not available in this Emacs")) |
| 389 | + |
| 390 | + ((or (null path) (not (file-exists-p path))) |
| 391 | + (list :status "error" |
| 392 | + :message (format "File not found: %s" (or path uri)))) |
| 393 | + |
| 394 | + (t |
| 395 | + (with-current-buffer (eca-editor--file-buffer path) |
| 396 | + (let ((workspaces (eca-editor--lsp-buffer-workspaces))) |
| 397 | + (cond |
| 398 | + ;; Workspace attached but the server has not finished the |
| 399 | + ;; initialize handshake yet: requests would fail with a |
| 400 | + ;; misleading capability error, so ask the server to retry. |
| 401 | + ((and workspaces |
| 402 | + (not (eca-editor--lsp-workspaces-ready-p workspaces))) |
| 403 | + (list :status "starting")) |
| 404 | + |
| 405 | + (workspaces |
| 406 | + (condition-case err |
| 407 | + (progn |
| 408 | + (eca-editor--lsp-request-async |
| 409 | + method |
| 410 | + (append (list :textDocument (list :uri (eca--path-to-uri path)) |
| 411 | + :position (list :line (max 0 (1- (or (plist-get position :line) 1))) |
| 412 | + :character (max 0 (1- (or (plist-get position :character) 1))))) |
| 413 | + extra-params) |
| 414 | + (lambda (result) |
| 415 | + (eca-api-send-request-response |
| 416 | + session request |
| 417 | + (list :status "success" |
| 418 | + :locations (eca-editor--lsp-locations->eca result)))) |
| 419 | + (lambda (err) |
| 420 | + (eca-api-send-request-response |
| 421 | + session request |
| 422 | + (list :status "error" |
| 423 | + :message (eca-editor--lsp-error-message err))))) |
| 424 | + :async) |
| 425 | + ;; lsp-mode can signal synchronously, e.g. the capability |
| 426 | + ;; check; answer error instead of dying in the filter. |
| 427 | + (error (list :status "error" |
| 428 | + :message (error-message-string err))))) |
| 429 | + |
| 430 | + ((eca-editor--lsp-known-root-p path) |
| 431 | + (condition-case err |
| 432 | + (if (eca-editor--lsp-start) |
| 433 | + (list :status "starting") |
| 434 | + (list :status "no-server" |
| 435 | + :message "No installed lsp-mode client for this file's major mode")) |
| 436 | + (error (list :status "no-server" |
| 437 | + :message (error-message-string err))))) |
| 438 | + |
| 439 | + (t |
| 440 | + (list :status "no-server" |
| 441 | + :message "No lsp-mode workspace for this file; start lsp in its project once"))))))))) |
| 442 | + |
| 443 | +(defun eca-editor-get-definition (session request params) |
| 444 | + "Handle the `editor/getDefinition' server REQUEST for SESSION. |
| 445 | +PARAMS contain the file uri and the 1-based symbol position." |
| 446 | + (eca-editor--nav-request session request params "textDocument/definition" nil)) |
| 447 | + |
| 448 | +(defun eca-editor-get-references (session request params) |
| 449 | + "Handle the `editor/getReferences' server REQUEST for SESSION. |
| 450 | +PARAMS contain the file uri, the 1-based symbol position and an |
| 451 | +optional includeDeclaration flag that defaults to true." |
| 452 | + (let ((include-declaration (if (plist-member params :includeDeclaration) |
| 453 | + (and (plist-get params :includeDeclaration) t) |
| 454 | + t))) |
| 455 | + (eca-editor--nav-request |
| 456 | + session request params "textDocument/references" |
| 457 | + (list :context (list :includeDeclaration (or include-declaration :json-false)))))) |
| 458 | + |
264 | 459 | (provide 'eca-editor) |
265 | 460 | ;;; eca-editor.el ends here |
0 commit comments