Skip to content

Commit

Permalink
Sync eglot.el and eglot-tests.el from upstream
Browse files Browse the repository at this point in the history
  • Loading branch information
joaotavora committed Jan 22, 2025
1 parent 819a5d1 commit 31fe2d3
Show file tree
Hide file tree
Showing 2 changed files with 85 additions and 97 deletions.
3 changes: 1 addition & 2 deletions eglot-tests.el
Original file line number Diff line number Diff line change
Expand Up @@ -442,7 +442,7 @@ directory hierarchy."
"Test basic symlink support."
(skip-unless (executable-find "clangd"))
;; MS-Windows either fails symlink creation or pops up UAC prompts.
(skip-when (eq system-type 'windows-nt))
(skip-unless (not (eq system-type 'windows-nt)))
(eglot--with-fixture
`(("symlink-project" .
(("main.cpp" . "#include\"foo.h\"\nint main() { return foo(); }")
Expand Down Expand Up @@ -798,7 +798,6 @@ int main() {
(insert "foo")
(company-mode)
(company-complete)
(should (looking-back "fooba"))
(should (= 2 (length company-candidates)))
;; this last one is brittle, since there it is possible that
;; clangd will change the representation of this candidate
Expand Down
179 changes: 84 additions & 95 deletions eglot.el
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
;; Maintainer: João Távora <joaotavora@gmail.com>
;; URL: https://github.com/joaotavora/eglot
;; Keywords: convenience, languages
;; Package-Requires: ((emacs "26.3") (compat "27.1") (eldoc "1.14.0") (external-completion "0.1") (flymake "1.2.1") (jsonrpc "1.0.24") (project "0.9.8") (seq "2.23") (track-changes "1.2") (xref "1.6.2"))
;; Package-Requires: ((emacs "26.3") (eldoc "1.14.0") (external-completion "0.1") (flymake "1.2.1") (jsonrpc "1.0.24") (project "0.9.8") (seq "2.23") (xref "1.6.2"))

;; This is a GNU ELPA :core package. Avoid adding functionality
;; that is not available in the version of Emacs recorded above or any
Expand Down Expand Up @@ -108,8 +108,6 @@
(require 'text-property-search nil t)
(require 'diff-mode)
(require 'diff)
(require 'track-changes)
(require 'compat)

;; These dependencies are also GNU ELPA core packages. Because of
;; bug#62576, since there is a risk that M-x package-install, despite
Expand Down Expand Up @@ -200,8 +198,8 @@ path of the PROGRAM that was chosen (interactively or
automatically)."
(lambda (&optional interactive _project)
;; JT@2021-06-13: This function is way more complicated than it
;; could be because it accounts for the fact that Compat's
;; `executable-find' may take much longer to execute on
;; could be because it accounts for the fact that
;; `eglot--executable-find' may take much longer to execute on
;; remote files.
(let* ((listified (cl-loop for a in alternatives
collect (if (listp a) a (list a))))
Expand All @@ -213,7 +211,7 @@ automatically)."
nil)
(interactive
(let* ((augmented (mapcar (lambda (a)
(let ((found (compat-call executable-find
(let ((found (eglot--executable-find
(car a) t)))
(and found
(cons (car a) (cons found (cdr a))))))
Expand All @@ -233,7 +231,7 @@ automatically)."
nil))))
(t
(cl-loop for (p . args) in listified
for probe = (compat-call executable-find p t)
for probe = (eglot--executable-find p t)
when probe return (cons probe args)
finally (funcall err)))))))

Expand Down Expand Up @@ -609,6 +607,11 @@ This can be useful when using docker to run a language server.")

(defconst eglot--{} (make-hash-table :size 0) "The empty JSON object.")

(defun eglot--executable-find (command &optional remote)
"Like Emacs 27's `executable-find', ignore REMOTE on Emacs 26."
(if (>= emacs-major-version 27) (executable-find command remote)
(executable-find command)))

(defun eglot--accepted-formats ()
(if (and (not eglot-prefer-plaintext) (fboundp 'gfm-view-mode))
["markdown" "plaintext"] ["plaintext"]))
Expand Down Expand Up @@ -791,7 +794,7 @@ compile time if an undeclared LSP interface is used."))
"Destructure OBJECT, binding VARS in BODY.
VARS is ([(INTERFACE)] SYMS...)
Honor `eglot-strict-mode'."
(declare (indent 2) (debug (sexp sexp &rest form)))
(declare (indent 2) (debug (sexp form &rest form)))
(let ((interface-name (if (consp (car vars))
(car (pop vars))))
(object-once (make-symbol "object-once"))
Expand Down Expand Up @@ -1333,7 +1336,7 @@ be guessed."
main-mode base-prompt))
((and program
(not (file-name-absolute-p program))
(not (compat-call executable-find program t)))
(not (eglot--executable-find program t)))
(if full-program-invocation
(concat (eglot--format
"[eglot] I guess you want to run `%s'"
Expand Down Expand Up @@ -1631,7 +1634,8 @@ This docstring appeases checkdoc, that's all."
:clientInfo
(append
'(:name "Eglot")
(let ((v (package-get-version)))
(let ((v (and (functionp 'package-get-version)
(package-get-version))))
(and v (list :version v))))
;; Maybe turn trampy `/ssh:foo@bar:/path/to/baz.py'
;; into `/path/to/baz.py', so LSP groks it.
Expand Down Expand Up @@ -1710,7 +1714,10 @@ in project `%s'."
;;;
(defun eglot--format (format &rest args)
"Like `format`, but substitutes quotes."
(apply #'format (substitute-quotes format) args))
(apply #'format (if (functionp 'substitute-quotes)
(substitute-quotes format)
format)
args))

(defun eglot--error (format &rest args)
"Error out with FORMAT with ARGS."
Expand Down Expand Up @@ -1788,24 +1795,6 @@ LBP defaults to `eglot--bol'."
:character (progn (when pos (goto-char pos))
(funcall eglot-current-linepos-function)))))

(defun eglot--virtual-pos-to-lsp-position (pos string)
"Return the LSP position at the end of STRING if it were inserted at POS."
(eglot--widening
(goto-char pos)
(forward-line 0)
;; LSP line is zero-origin; Emacs is one-origin.
(let ((posline (1- (line-number-at-pos nil t)))
(linebeg (buffer-substring (point) pos))
(colfun eglot-current-linepos-function))
;; Use a temp buffer because:
;; - I don't know of a fast way to count newlines in a string.
;; - We currently don't have `eglot-current-linepos-function' for strings.
(with-temp-buffer
(insert linebeg string)
(goto-char (point-max))
(list :line (+ posline (1- (line-number-at-pos nil t)))
:character (funcall colfun))))))

(defvar eglot-move-to-linepos-function #'eglot-move-to-utf-16-linepos
"Function to move to a position within a line reported by the LSP server.
Expand Down Expand Up @@ -1916,9 +1905,10 @@ MARKUP is either an LSP MarkedString or MarkupContent object."
(font-lock-ensure)
(goto-char (point-min))
(let ((inhibit-read-only t))
(while (setq match (text-property-search-forward 'invisible))
(delete-region (prop-match-beginning match)
(prop-match-end match))))
(when (fboundp 'text-property-search-forward)
(while (setq match (text-property-search-forward 'invisible))
(delete-region (prop-match-beginning match)
(prop-match-end match)))))
(string-trim (buffer-string))))))

(defun eglot--read-server (prompt &optional dont-if-just-the-one)
Expand Down Expand Up @@ -2012,8 +2002,6 @@ For example, to keep your Company customization, add the symbol
"A hook run by Eglot after it started/stopped managing a buffer.
Use `eglot-managed-p' to determine if current buffer is managed.")

(defvar-local eglot--track-changes nil)

(define-minor-mode eglot--managed-mode
"Mode for source buffers managed by some Eglot project."
:init-value nil :lighter nil :keymap eglot-mode-map :interactive nil
Expand All @@ -2027,10 +2015,8 @@ Use `eglot-managed-p' to determine if current buffer is managed.")
("utf-8"
(eglot--setq-saving eglot-current-linepos-function #'eglot-utf-8-linepos)
(eglot--setq-saving eglot-move-to-linepos-function #'eglot-move-to-utf-8-linepos)))
(unless eglot--track-changes
(setq eglot--track-changes
(track-changes-register
#'eglot--track-changes-signal :disjoint t)))
(add-hook 'after-change-functions #'eglot--after-change nil t)
(add-hook 'before-change-functions #'eglot--before-change nil t)
(add-hook 'kill-buffer-hook #'eglot--managed-mode-off nil t)
;; Prepend "didClose" to the hook after the "nonoff", so it will run first
(add-hook 'kill-buffer-hook #'eglot--signal-textDocument/didClose nil t)
Expand Down Expand Up @@ -2064,6 +2050,8 @@ Use `eglot-managed-p' to determine if current buffer is managed.")
(eldoc-mode 1))
(cl-pushnew (current-buffer) (eglot--managed-buffers (eglot-current-server))))
(t
(remove-hook 'after-change-functions #'eglot--after-change t)
(remove-hook 'before-change-functions #'eglot--before-change t)
(remove-hook 'kill-buffer-hook #'eglot--managed-mode-off t)
(remove-hook 'kill-buffer-hook #'eglot--signal-textDocument/didClose t)
(remove-hook 'before-revert-hook #'eglot--signal-textDocument/didClose t)
Expand Down Expand Up @@ -2093,10 +2081,7 @@ Use `eglot-managed-p' to determine if current buffer is managed.")
(delq (current-buffer) (eglot--managed-buffers server)))
(when (and eglot-autoshutdown
(null (eglot--managed-buffers server)))
(eglot-shutdown server))))
(when eglot--track-changes
(track-changes-unregister eglot--track-changes)
(setq eglot--track-changes nil)))))
(eglot-shutdown server)))))))

(defun eglot--managed-mode-off ()
"Turn off `eglot--managed-mode' unconditionally."
Expand Down Expand Up @@ -2648,67 +2633,71 @@ buffer."
`(:triggerKind 2 :triggerCharacter ,trigger) `(:triggerKind 1)))))

(defvar-local eglot--recent-changes nil
"Recent buffer changes as collected by `eglot--track-changes-fetch'.")
"Recent buffer changes as collected by `eglot--before-change'.")

(cl-defmethod jsonrpc-connection-ready-p ((_server eglot-lsp-server) _what)
"Tell if SERVER is ready for WHAT in current buffer."
(and (cl-call-next-method) (not eglot--recent-changes)))

(defvar-local eglot--change-idle-timer nil "Idle timer for didChange signals.")

(defun eglot--before-change (beg end)
"Hook onto `before-change-functions' with BEG and END."
(when (listp eglot--recent-changes)
;; Records BEG and END, crucially convert them into LSP
;; (line/char) positions before that information is lost (because
;; the after-change thingy doesn't know if newlines were
;; deleted/added). Also record markers of BEG and END
;; (github#259)
(push `(,(eglot--pos-to-lsp-position beg)
,(eglot--pos-to-lsp-position end)
(,beg . ,(copy-marker beg nil))
(,end . ,(copy-marker end t)))
eglot--recent-changes)))

(defvar eglot--document-changed-hook '(eglot--signal-textDocument/didChange)
"Internal hook for doing things when the document changes.")

(defun eglot--track-changes-fetch (id)
(if (eq eglot--recent-changes :pending) (setq eglot--recent-changes nil))
(track-changes-fetch
id (lambda (beg end before)
(cl-incf eglot--versioned-identifier)
(cond
((eq eglot--recent-changes :emacs-messup) nil)
((eq before 'error) (setf eglot--recent-changes :emacs-messup))
(t (push `(,(eglot--pos-to-lsp-position beg)
,(eglot--virtual-pos-to-lsp-position beg before)
,(length before)
,(buffer-substring-no-properties beg end))
eglot--recent-changes))))))

(defun eglot--add-one-shot-hook (hook function &optional append local)
"Like `add-hook' but calls FUNCTION only once."
(let* ((fname (make-symbol (format "eglot--%s-once" function)))
(fun (lambda (&rest args)
(remove-hook hook fname local)
(apply function args))))
(fset fname fun)
(add-hook hook fname append local)))

(defun eglot--track-changes-signal (id &optional distance)
(cond
(distance
;; When distance is <100, we may as well coalesce the changes.
(when (> distance 100) (eglot--track-changes-fetch id)))
(eglot--recent-changes nil)
;; Note that there are pending changes, for the benefit of those
;; who check it as a boolean.
(t (setq eglot--recent-changes :pending)))
(defun eglot--after-change (beg end pre-change-length)
"Hook onto `after-change-functions'.
Records BEG, END and PRE-CHANGE-LENGTH locally."
(cl-incf eglot--versioned-identifier)
(pcase (car-safe eglot--recent-changes)
(`(,lsp-beg ,lsp-end
(,b-beg . ,b-beg-marker)
(,b-end . ,b-end-marker))
;; github#259 and github#367: with `capitalize-word' & friends,
;; `before-change-functions' records the whole word's `b-beg' and
;; `b-end'. Similarly, when `fill-paragraph' coalesces two
;; lines, `b-beg' and `b-end' mark end of first line and end of
;; second line, resp. In both situations, `beg' and `end'
;; received here seemingly contradict that: they will differ by 1
;; and encompass the capitalized character or, in the coalescing
;; case, the replacement of the newline with a space. We keep
;; both markers and positions to detect and correct this. In
;; this specific case, we ignore `beg', `len' and
;; `pre-change-len' and send richer information about the region
;; from the markers. I've also experimented with doing this
;; unconditionally but it seems to break when newlines are added.
(if (and (= b-end b-end-marker) (= b-beg b-beg-marker)
(or (/= beg b-beg) (/= end b-end)))
(setcar eglot--recent-changes
`(,lsp-beg ,lsp-end ,(- b-end-marker b-beg-marker)
,(buffer-substring-no-properties b-beg-marker
b-end-marker)))
(setcar eglot--recent-changes
`(,lsp-beg ,lsp-end ,pre-change-length
,(buffer-substring-no-properties beg end)))))
(_ (setf eglot--recent-changes :emacs-messup)))
(when eglot--change-idle-timer (cancel-timer eglot--change-idle-timer))
(setq eglot--change-idle-timer
(run-with-idle-timer
eglot-send-changes-idle-time nil
(lambda (buf)
(eglot--when-live-buffer buf
(when eglot--managed-mode
(if (track-changes-inconsistent-state-p)
;; Not a good time (e.g. in the middle of Quail thingy,
;; bug#70541): reschedule for the next idle period.
(eglot--add-one-shot-hook
'post-command-hook
(lambda ()
(eglot--when-live-buffer buf
(eglot--track-changes-signal id))))
(run-hooks 'eglot--document-changed-hook)
(setq eglot--change-idle-timer nil)))))
(current-buffer))))
(let ((buf (current-buffer)))
(setq eglot--change-idle-timer
(run-with-idle-timer
eglot-send-changes-idle-time
nil (lambda () (eglot--when-live-buffer buf
(when eglot--managed-mode
(run-hooks 'eglot--document-changed-hook)
(setq eglot--change-idle-timer nil))))))))

(defvar-local eglot-workspace-configuration ()
"Configure LSP servers specifically for a given project.
Expand Down Expand Up @@ -2812,7 +2801,6 @@ When called interactively, use the currently active server"

(defun eglot--signal-textDocument/didChange ()
"Send textDocument/didChange to server."
(eglot--track-changes-fetch eglot--track-changes)
(when eglot--recent-changes
(let* ((server (eglot--current-server-or-lose))
(sync-capability (eglot-server-capable :textDocumentSync))
Expand All @@ -2838,7 +2826,6 @@ When called interactively, use the currently active server"
(defun eglot--signal-textDocument/didOpen ()
"Send textDocument/didOpen to server."
;; Flush any potential pending change.
(eglot--track-changes-fetch eglot--track-changes)
(setq eglot--recent-changes nil
eglot--versioned-identifier 0
eglot--TextDocumentIdentifier-cache nil)
Expand Down Expand Up @@ -3276,7 +3263,9 @@ for which LSP on-type-formatting should be requested."
(dolist (c comps) (eglot--dumb-flex pattern c completion-ignore-case))
(all-completions
""
comps
;; copy strings, as some older emacs
;; versions will destroy properties.
(mapcar #'substring comps)
(lambda (proxy)
(let* ((item (get-text-property 0 'eglot--lsp-item proxy))
(filterText (plist-get item :filterText)))
Expand Down

0 comments on commit 31fe2d3

Please # to comment.