-
Notifications
You must be signed in to change notification settings - Fork 56
Expand file tree
/
Copy patheca-editor.el
More file actions
582 lines (539 loc) · 26.5 KB
/
Copy patheca-editor.el
File metadata and controls
582 lines (539 loc) · 26.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
;;; eca-editor.el --- ECA (Editor Code Assistant) editor -*- lexical-binding: t; -*-
;; Copyright (C) 2025 Eric Dallo
;;
;; SPDX-License-Identifier: Apache-2.0
;;
;; This file is not part of GNU Emacs.
;;
;;; Commentary:
;;
;; The ECA (Editor Code Assistant) editor.
;;
;;; Code:
(require 'dash)
(require 'f)
(require 'xref)
(require 'lsp-mode nil t)
(require 'flymake nil t)
(require 'flycheck nil t)
(require 'ht nil t)
(require 'eca-api)
(require 'eca-util)
(declare-function lsp-diagnostics "lsp-mode" (current-workspace?))
(declare-function lsp-get "lsp-mode" (from key))
(declare-function lsp "lsp-mode" (&optional arg))
(declare-function lsp-request-async "lsp-mode" (method params callback &rest plist))
(declare-function lsp-workspaces "lsp-mode" ())
(declare-function lsp-session "lsp-mode" ())
(declare-function lsp-session-folders "lsp-mode" (session))
(declare-function lsp--workspace-server-capabilities "lsp-mode" (workspace))
(declare-function lsp--find-clients "lsp-mode" ())
(declare-function ht-select "ht" (function table))
(declare-function flymake-diagnostic-code "flymake" (diagnostic))
(declare-function flycheck-running-p "flycheck" ())
(declare-function flycheck-buffer "flycheck" ())
(declare-function flycheck-error-filename "flycheck" (err))
(declare-function flycheck-error-line "flycheck" (err))
(declare-function flycheck-error-column "flycheck" (err))
(declare-function flycheck-error-end-line "flycheck" (err))
(declare-function flycheck-error-end-column "flycheck" (err))
(declare-function flycheck-error-level "flycheck" (err))
(declare-function flycheck-error-message "flycheck" (err))
(declare-function flycheck-error-id "flycheck" (err))
(declare-function flycheck-error-checker "flycheck" (err))
(defun eca-editor--lsp-to-eca-severity (severity)
"Convert lsp SEVERITY to eca one."
(pcase severity
(1 "error")
(2 "warning")
(3 "info")
(4 "hint")
(_ "unknown")))
(defun eca-editor--flymake-to-eca-severity (severity)
"Convert flymake SEVERITY to eca one."
(pcase severity
((or 'flymake-error ':error 'eglot-error) "error")
((or 'flymake-warning ':warning 'eglot-warning) "warning")
((or 'flymake-note ':note 'eglot-note) "information")
(_ "unknown")))
(defun eca-editor--flycheck-active-in-locus-p (locus)
"Return non-nil if LOCUS is a live buffer with `flycheck-mode' on.
Used to suppress flymake diagnostics for buffers where flycheck is
already the active diagnostic surface, mirroring how the flycheck
branch suppresses its own output in lsp-managed buffers. The
resulting precedence is: lsp-mode > flycheck > flymake."
(and (buffer-live-p locus)
(with-current-buffer locus
(bound-and-true-p flycheck-mode))))
(defun eca-editor--flycheck-to-eca-severity (level)
"Convert flycheck LEVEL to eca severity."
(pcase level
('error "error")
('warning "warning")
('info "information")
(_ "unknown")))
(defun eca-editor--lsp-mode-diagnostics (uri workspace)
"Find all `lsp-mode` diagnostics found for WORKSPACE.
If URI is nil find all diagnostics otherwise filter to that uri."
(let* ((eca-diagnostics '())
(all-diagnostics (car (--keep (when (and (buffer-file-name it)
(f-ancestor-of? workspace (buffer-file-name it)))
(with-current-buffer it
(lsp-diagnostics t)))
(buffer-list))))
(diagnostics (if uri
(ht-select (lambda (k _v) (string= k (eca--uri-to-path uri))) all-diagnostics)
all-diagnostics)))
(when diagnostics
(maphash (lambda (path lsp-diagnostics)
(let ((uri (eca--path-to-uri path)))
(--map (let* ((range (lsp-get it :range))
(start-r (lsp-get range :start))
(end-r (lsp-get range :end)))
(push (list :uri uri
:severity (eca-editor--lsp-to-eca-severity (lsp-get it :severity))
:code (when-let ((code (lsp-get it :code)))
(if (symbolp code)
(symbol-name code)
(format "%s" code)))
:range (list :start (list :line (lsp-get start-r :line)
:character (lsp-get start-r :character))
:end (list :line (lsp-get end-r :line)
:character (lsp-get end-r :character)))
:source (when-let ((source (lsp-get it :source)))
(if (symbolp source)
(symbol-name source)
(format "%s" source)))
:message (lsp-get it :message))
eca-diagnostics))
lsp-diagnostics)))
diagnostics))
eca-diagnostics))
(defun eca-editor--flymake-diagnostics (uri workspace)
"Find all flymake diagnostics found for WORKSPACE.
If URI is nil find all diagnostics otherwise filter to that uri."
(let ((default-directory workspace))
(--keep
(let* ((locus (flymake-diagnostic-buffer it))
(buffer-p (bufferp locus))
(file-path (if buffer-p (buffer-file-name locus) locus))
(diag-uri (eca--path-to-uri file-path))
(beg (flymake-diagnostic-beg it))
(end (flymake-diagnostic-end it)))
(when (and (or (null uri) (string= uri diag-uri))
(not (eca-editor--flycheck-active-in-locus-p locus)))
(let ((range (if buffer-p
(with-current-buffer locus
(save-excursion
(list :start (list :line (progn (goto-char beg) (line-number-at-pos))
:character (current-column))
:end (list :line (progn (goto-char (or end beg)) (line-number-at-pos))
:character (current-column)))))
;; file path locus - beg/end are already (line . col)
(list :start (list :line (car beg) :character (cdr beg))
:end (list :line (if end (car end) (car beg))
:character (if end (cdr end) (cdr beg)))))))
(list :uri diag-uri
:severity (eca-editor--flymake-to-eca-severity (flymake-diagnostic-type it))
:code (when-let ((code (and (fboundp 'flymake-diagnostic-code)
(flymake-diagnostic-code it))))
(if (symbolp code) (symbol-name code) (format "%s" code)))
:range range
:source (when-let ((backend (flymake-diagnostic-backend it)))
(if (symbolp backend) (symbol-name backend) (format "%s" backend)))
:message (flymake-diagnostic-text it)))))
(flymake--project-diagnostics))))
(defun eca-editor--flycheck-diagnostics (uri workspaces)
"Collect flycheck diagnostics from buffers in WORKSPACES.
Skips buffers where `lsp-mode' is active to avoid duplicates.
If URI is non-nil, filter to that uri."
(let ((diagnostics '()))
(dolist (buf (buffer-list))
(when-let ((file (buffer-file-name buf)))
(let ((file-uri (eca--path-to-uri file)))
(when (and (or (null uri) (string= uri file-uri))
(seq-some (lambda (ws)
(f-ancestor-of? ws file))
workspaces))
(with-current-buffer buf
(when (and (bound-and-true-p flycheck-mode)
(not (bound-and-true-p lsp-mode)))
(dolist (err flycheck-current-errors)
(let* ((err-file (or (flycheck-error-filename err)
file))
(err-uri (eca--path-to-uri err-file))
(line (flycheck-error-line err))
(col (flycheck-error-column err))
(end-line (flycheck-error-end-line err))
(end-col (flycheck-error-end-column err))
(start-line (max 0 (1- (or line 1))))
(start-char (max 0 (1- (or col 1))))
(e-line (max 0 (1- (or end-line line 1))))
(e-char (max 0 (1- (or end-col col 1)))))
(push (list :uri err-uri
:severity (eca-editor--flycheck-to-eca-severity
(flycheck-error-level err))
:code (when-let ((id (flycheck-error-id err)))
(format "%s" id))
:range (list :start (list :line start-line
:character start-char)
:end (list :line e-line
:character e-char))
:source (when-let ((checker (flycheck-error-checker err)))
(symbol-name checker))
:message (flycheck-error-message err))
diagnostics)))))))))
diagnostics))
(defun eca-editor--wait-for-flycheck (timeout)
"Wait for flycheck to finish, up to TIMEOUT seconds."
(let ((deadline (+ (float-time) timeout)))
(while (and (flycheck-running-p)
(< (float-time) deadline))
(sit-for 0.2))))
(defun eca-editor--ensure-diagnostics-fresh (uri)
"Ensure diagnostics for URI are fresh.
Visits the buffer if needed, reverts if the file
changed on disk, and waits for checkers to finish."
(when uri
(let* ((file (eca--uri-to-path uri))
(buf (or (find-buffer-visiting file)
(find-file-noselect file))))
(with-current-buffer buf
;; Revert if file on disk is newer than buffer
(when (and (buffer-file-name)
(not (verify-visited-file-modtime buf)))
(revert-buffer t t t))
(font-lock-ensure)
(cond
;; lsp-mode: diagnostics are pushed async by
;; the server, we can only wait.
((bound-and-true-p lsp-mode)
(sit-for 2))
;; flycheck: trigger and poll for completion
((bound-and-true-p flycheck-mode)
(flycheck-buffer)
(eca-editor--wait-for-flycheck 3.0))
;; flymake: trigger and wait
((bound-and-true-p flymake-mode)
(flymake-start)
(sit-for 2))
;; fallback: wait for mode hooks to settle
(t
(sit-for 2)))))))
(defun eca-editor--collect-all-diagnostics (uri workspaces)
"Collect diagnostics from all backends for WORKSPACES.
If URI is non-nil, filter to that uri."
(let ((diagnostics '()))
;; lsp-mode: workspace-level diagnostics
(when (featurep 'lsp-mode)
(seq-doseq (workspace workspaces)
(setq diagnostics
(append diagnostics
(eca-editor--lsp-mode-diagnostics
uri workspace)))))
;; flycheck: buffer-level, non-LSP buffers only
(when (featurep 'flycheck)
(setq diagnostics
(append diagnostics
(eca-editor--flycheck-diagnostics
uri workspaces))))
;; flymake: for eglot users (when lsp-mode is absent)
(when (and (featurep 'flymake)
(not (featurep 'lsp-mode)))
(seq-doseq (workspace workspaces)
(setq diagnostics
(append diagnostics
(eca-editor--flymake-diagnostics
uri workspace)))))
diagnostics))
(defun eca-editor-get-diagnostics (session params)
"Return all diagnostics for SESSION from PARAMS."
(let* ((uri (plist-get params :uri))
(workspaces (eca--session-workspace-folders session)))
(eca-editor--ensure-diagnostics-fresh uri)
(list :diagnostics
(vconcat (eca-editor--collect-all-diagnostics
uri workspaces)))))
(defun eca-editor--lsp-nav-available-p ()
"Return non-nil when lsp-mode is available for navigation requests."
(featurep 'lsp-mode))
(defun eca-editor--lsp-buffer-workspaces ()
"Return the lsp-mode workspaces associated with the current buffer."
(and (fboundp 'lsp-workspaces) (lsp-workspaces)))
(defun eca-editor--lsp-workspaces-ready-p (workspaces)
"Return non-nil when some of WORKSPACES finished initializing.
A lsp-mode workspace only gets its server capabilities after the
initialize handshake, so none having them means the server is
still starting. Fails open: workspaces that cannot be inspected
count as ready so the request is still attempted."
(seq-some (lambda (workspace)
(condition-case nil
(and (lsp--workspace-server-capabilities workspace) t)
(error t)))
workspaces))
(defun eca-editor--lsp-known-root-p (path)
"Return non-nil if PATH is inside a folder known to the lsp-mode session."
(and (fboundp 'lsp-session)
(fboundp 'lsp-session-folders)
(seq-some (lambda (folder)
(f-ancestor-of? folder path))
(lsp-session-folders (lsp-session)))))
(defun eca-editor--lsp-start ()
"Start lsp-mode in the current buffer when an installed client matches.
Return non-nil when a start was attempted, nil when no installed
lsp-mode client supports this buffer. Checking the clients first
avoids lsp-mode's interactive server-install prompt, which must
never fire from the process filter."
(when (or (not (fboundp 'lsp--find-clients))
(lsp--find-clients))
(lsp)
t))
(defun eca-editor--lsp-request-async (method params callback error-callback)
"Send async lsp-mode request METHOD with PARAMS.
Call CALLBACK with the result or ERROR-CALLBACK with the error."
(lsp-request-async method params callback
:error-handler error-callback
:mode 'detached))
(defun eca-editor--file-buffer (path)
"Return a buffer visiting PATH, opening it in background if needed."
(or (find-buffer-visiting path)
(let ((enable-local-variables :safe)
(large-file-warning-threshold nil))
(find-file-noselect path))))
(defun eca-editor--lsp-obj-get (obj key)
"Get keyword KEY from lsp result OBJ.
Supports both lsp-mode result representations: hash-tables with
string keys and plists with keyword keys."
(cond
((hash-table-p obj) (gethash (substring (symbol-name key) 1) obj))
((listp obj) (plist-get obj key))))
(defun eca-editor--lsp-position->eca (position)
"Convert 0-based lsp POSITION to a 1-based eca position."
(list :line (1+ (or (eca-editor--lsp-obj-get position :line) 0))
:character (1+ (or (eca-editor--lsp-obj-get position :character) 0))))
(defun eca-editor--lsp-location->eca (location)
"Convert a lsp Location or LocationLink LOCATION to an eca location."
(when location
(let ((uri (or (eca-editor--lsp-obj-get location :uri)
(eca-editor--lsp-obj-get location :targetUri)))
(range (or (eca-editor--lsp-obj-get location :range)
(eca-editor--lsp-obj-get location :targetSelectionRange)
(eca-editor--lsp-obj-get location :targetRange))))
(when (and uri range)
(list :uri uri
:range (list :start (eca-editor--lsp-position->eca
(eca-editor--lsp-obj-get range :start))
:end (eca-editor--lsp-position->eca
(eca-editor--lsp-obj-get range :end))))))))
(defun eca-editor--lsp-locations->eca (result)
"Normalize a lsp navigation RESULT into a vector of eca locations.
RESULT may be nil, a single Location, or a sequence of Location
or LocationLink."
(let ((locations (cond
((null result) nil)
((vectorp result) (append result nil))
((hash-table-p result) (list result))
((keywordp (car-safe result)) (list result))
(t result))))
(vconcat (delq nil (mapcar #'eca-editor--lsp-location->eca locations)))))
(defun eca-editor--lsp-error-message (err)
"Extract a human readable message from the lsp-mode error ERR."
(cond
((null err) "lsp-mode request failed")
((stringp err) err)
((or (hash-table-p err) (keywordp (car-safe err)))
(or (eca-editor--lsp-obj-get err :message) (format "%s" err)))
(t (format "%s" err))))
(defun eca-editor--include-declaration-p (params)
"Whether PARAMS ask to include the symbol declaration in references.
Defaults to true when absent. JSON false is parsed as nil by
`eca-api', distinguished from absence via `plist-member'."
(if (plist-member params :includeDeclaration)
(and (plist-get params :includeDeclaration) t)
t))
(defun eca-editor--xref-backend ()
"Return a usable xref backend for the current buffer, if any.
The default etags backend only counts when a tags table is
already loaded: consulting it otherwise prompts for a TAGS file,
which must never happen while answering a server request."
(let ((backend (xref-find-backend)))
(unless (and (eq backend 'etags)
(not (or (bound-and-true-p tags-file-name)
(bound-and-true-p tags-table-list))))
backend)))
(defun eca-editor--goto-position (position)
"Move point to the 1-based POSITION in the current buffer."
(widen)
(goto-char (point-min))
(forward-line (max 0 (1- (or (plist-get position :line) 1))))
(forward-char (min (max 0 (1- (or (plist-get position :character) 1)))
(- (line-end-position) (point)))))
(defun eca-editor--xref-marker->location (marker)
"Convert MARKER into an eca location with a 1-based point range."
(when-let ((buffer (marker-buffer marker))
(file (buffer-file-name (marker-buffer marker))))
(with-current-buffer buffer
(save-excursion
(save-restriction
(widen)
(goto-char marker)
(let ((position (list :line (line-number-at-pos nil t)
:character (1+ (- (point) (line-beginning-position))))))
(list :uri (eca--path-to-uri file)
:range (list :start position :end position))))))))
(defun eca-editor--xref-items->locations (items)
"Convert xref ITEMS into a vector of eca locations."
(let ((enable-local-variables :safe)
(large-file-warning-threshold nil))
(vconcat
(delq nil
(mapcar (lambda (item)
;; Resolving a location marker may visit the target
;; file and can fail e.g. for stale results pointing
;; at deleted files; drop those locations.
(condition-case nil
(eca-editor--xref-marker->location
(xref-location-marker (xref-item-location item)))
(error nil)))
items)))))
(defun eca-editor--xref-locations (backend position references-p include-declaration)
"Compute eca locations via xref BACKEND for the symbol at POSITION.
When REFERENCES-P find references, otherwise definitions. When
INCLUDE-DECLARATION is nil, reference locations matching a
definition of the symbol are filtered out, since LSP-backed xref
backends always include the declaration."
(save-excursion
(eca-editor--goto-position position)
(let ((identifier (xref-backend-identifier-at-point backend)))
(cond
((null identifier) (vector))
((not references-p)
(eca-editor--xref-items->locations
(xref-backend-definitions backend identifier)))
(t
(let ((references (append (eca-editor--xref-items->locations
(xref-backend-references backend identifier))
nil)))
(unless include-declaration
(let ((declarations (append (eca-editor--xref-items->locations
(xref-backend-definitions backend identifier))
nil)))
(setq references
(-remove (lambda (location) (member location declarations))
references))))
(vconcat references)))))))
(defun eca-editor--xref-fallback (session request params method no-server-response)
"Try answering the nav REQUEST of SESSION with the buffer's xref backend.
Covers eglot, etags with a loaded tags table and any custom
backend. METHOD decides between definitions and references and
PARAMS carry the position, mirroring the lsp-mode path. Schedule
the lookup outside the process filter and return `:async' when a
usable backend exists, otherwise return NO-SERVER-RESPONSE."
(if-let ((backend (eca-editor--xref-backend)))
(let ((buffer (current-buffer))
(position (plist-get params :position))
(references-p (string= method "textDocument/references"))
(include-declaration (eca-editor--include-declaration-p params)))
(run-at-time
0 nil
(lambda ()
(eca-api-send-request-response
session request
(condition-case err
(if (buffer-live-p buffer)
(with-current-buffer buffer
(list :status "success"
:locations (eca-editor--xref-locations
backend position references-p include-declaration)))
(list :status "error" :message "Buffer was killed"))
(error (list :status "error"
:message (error-message-string err)))))))
:async)
no-server-response))
(defun eca-editor--nav-request (session request params method extra-params)
"Ask the editor for the METHOD locations of the symbol in PARAMS.
Respond to the server REQUEST of SESSION following the status
contract of the `editor/getDefinition' and `editor/getReferences'
requests: success, starting, no-server or error. EXTRA-PARAMS
are appended to the lsp request params. Prefer lsp-mode, falling
back to the buffer's xref backend (eglot, etags, ...) when
lsp-mode cannot serve the file. Return the response plist for
sync answers or `:async' when the response will be sent later."
(let* ((uri (plist-get params :uri))
(position (plist-get params :position))
(path (and uri (eca--uri-to-path uri))))
(cond
((or (null path) (not (file-exists-p path)))
(list :status "error"
:message (format "File not found: %s" (or path uri))))
((not (eca-editor--lsp-nav-available-p))
(with-current-buffer (eca-editor--file-buffer path)
(eca-editor--xref-fallback
session request params method
(list :status "no-server"
:message "No lsp-mode and no xref backend available for this file"))))
(t
(with-current-buffer (eca-editor--file-buffer path)
(let ((workspaces (eca-editor--lsp-buffer-workspaces)))
(cond
;; Workspace attached but the server has not finished the
;; initialize handshake yet: requests would fail with a
;; misleading capability error, so ask the server to retry.
((and workspaces
(not (eca-editor--lsp-workspaces-ready-p workspaces)))
(list :status "starting"))
(workspaces
(condition-case err
(progn
(eca-editor--lsp-request-async
method
(append (list :textDocument (list :uri (eca--path-to-uri path))
:position (list :line (max 0 (1- (or (plist-get position :line) 1)))
:character (max 0 (1- (or (plist-get position :character) 1)))))
extra-params)
(lambda (result)
(eca-api-send-request-response
session request
(list :status "success"
:locations (eca-editor--lsp-locations->eca result))))
(lambda (err)
(eca-api-send-request-response
session request
(list :status "error"
:message (eca-editor--lsp-error-message err)))))
:async)
;; lsp-mode can signal synchronously, e.g. the capability
;; check; answer error instead of dying in the filter.
(error (list :status "error"
:message (error-message-string err)))))
((eca-editor--lsp-known-root-p path)
(condition-case err
(if (eca-editor--lsp-start)
(list :status "starting")
(eca-editor--xref-fallback
session request params method
(list :status "no-server"
:message "No installed lsp-mode client for this file's major mode and no xref backend")))
(error (eca-editor--xref-fallback
session request params method
(list :status "no-server"
:message (error-message-string err))))))
(t
(eca-editor--xref-fallback
session request params method
(list :status "no-server"
:message "No lsp-mode workspace and no xref backend for this file; start lsp or eglot in its project once"))))))))))
(defun eca-editor-get-definition (session request params)
"Handle the `editor/getDefinition' server REQUEST for SESSION.
PARAMS contain the file uri and the 1-based symbol position."
(eca-editor--nav-request session request params "textDocument/definition" nil))
(defun eca-editor-get-references (session request params)
"Handle the `editor/getReferences' server REQUEST for SESSION.
PARAMS contain the file uri, the 1-based symbol position and an
optional includeDeclaration flag that defaults to true."
(let ((include-declaration (eca-editor--include-declaration-p params)))
(eca-editor--nav-request
session request params "textDocument/references"
(list :context (list :includeDeclaration (or include-declaration :json-false))))))
(provide 'eca-editor)
;;; eca-editor.el ends here