-
-
Notifications
You must be signed in to change notification settings - Fork 912
Calculate wrong end position after kill a line #114
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
I've run into this problem, as well. Unfortunately, I think it's actually not possible for Emacs to correctly implement the protocol - at least not using text change hooks. I'm reposting some analysis from microsoft/language-server-protocol#9 (comment) inline below. Consider the case of a single-line deletion. Here are the buffer contents at time 1. 1234 And here are the buffer contents at time 2, after line 1 is deleted. (Note that a newline character is at position 5). 6789 From the Emacs documentation on Change Hooks: Three arguments are passed to each function: the positions of So after the line deletion, our after-change-function is invoked with the following arguments:
The desired payload of the TextDocumentContentChangeEvent for this edit looks like this:
range.start can be calculated given the value of start and the current buffer contents. rangeLength is equal to the value of length. The problem is in calculating range.end; there is no way to reconstruct the pre-edit state of the buffer given the information available. As proof, instead of 1234 consider the following, alternate, prior state of the buffer: 1 Deleting the first 2 lines would produce the same buffer state as before: 6789 And the arguments to our after-change-function would be the same:
But the correct payload for the TextDocumentContentChangeEvent corresponding to this change is:
(Note the difference in the Thus, the best we can do using Emacs Text Hooks is to correctly specify the range.start and rangeLength and populate range.end with some garbage value. Given a range.start, knowledge of either rangeLength or range.end should be sufficient for the server to update its representation of the document. As a proof of concept, I have a local branch of this repository which populates a garbage end position for the TextDocumentContentChangeEvent, like this:
And I've implemented logic in a local branch of https://github.com/palantir/python-language-server to calculate the end position based on range.start and the rangeLength if range.end is invalid. This works very well and addresses the problem raised in this issue. So my question to the maintainers is: would you be interested in a patch in this repository to set an invalid end position? The obvious drawback is that each language server implementation will need to implement the logic to calculate the range.end if it's missing or invalid. However, I think making this failure mode explicit is preferable to the current state, in which the server's representation of the document silently diverges from the true state in the editor and produces bizarre behavior. Also, please do chime in on microsoft/language-server-protocol#9 and advocate for amending the protocol to formalize this behavior by allowing clients like Emacs to omit either range.end or rangeLength in the TextDocumentContentChangeEvent. |
Thanks for explaining this. IMHO, setting
One possible option might also be to add a new hook to Emacs itself, which sends the line and column positions instead of the numerical points for (@alanz, thoughts?) |
This is a bit extreme
The current implementation sends a change message per The second option of calculating a diff in emacs would be unworkable in practice, I suspect, since this is a very high frequency event. Given that emacs is one of the major clients, I think it would be worth asking the protocol to support a change option that matches what is in emacs, so that we end up with the natural behaviour. Alternatively we should discuss server behaviour around this, in the sense of buffering change requests there before launching an expensive diagnostic process. This is essentially identical to accumulating the changes in emacs, but the accumulation is in the server. |
Agreed that this definitely is more work for the server, so unless the protocol is amended this would be very fragile. But FWIW, it's relatively straightforward to implement this (unless of course my implementation is bugged, which is entirely possible). Here's how I've done it in the python-language-server: def _calculate_end_position(self, start_line, start_col, range_length):
'''Calculate line and column-based position for the end of range'''
col_offset = start_col
i = start_line
def range_spans_this_line(i, range_length, col_offset):
return i < len(self.lines) and \
range_length > len(self.lines[i]) - col_offset
while range_spans_this_line(i, range_length, col_offset):
range_length -= len(self.lines[i]) - col_offset
col_offset = 0
i += 1
assert i < len(self.lines), "End of range exceeds end of document - no more lines!"
assert range_length <= len(self.lines[i]) - col_offset, "End of range exceeds end of document - no more columns!"
# the range ends on this line, just move the offset
end_line = i
end_col = col_offset + range_length
return end_line, end_col I agree with @alanz's assessment - the best option would be to formalize this in the protocol by allowing clients to set exactly one of range.end or rangeLength, since either is redundant given the other. Shall we open an issue for this in https://github.com/Microsoft/language-server-protocol? So far all the discussion has piggybacked off of microsoft/language-server-protocol#9 In the meantime, capturing the state of the buffer during the change hook is a clever idea. Perhaps we should prototype it and see if it adds any noticeable latency? |
Can we look up (defun delete-change (start end length)
(if (and (> length 0) (listp buffer-undo-list))
;;lenght > 0 is deleting something
;;So the first entry of buffer-undo-list is (TEXT . END)
;;We can count how many \n are deleted to caculate end-point :line
(let ((d (split-string (substring-no-properties
(car (car buffer-undo-list))) "\n"))
(start-p (lsp-point-to-position start))
(end-p '(:line 0 :character 0)))
(plist-put end-p :line (+ (plist-get start-p :line) (length d) -1))
(plist-put end-p :character (if (> (length d) 1)
(length (car (last d)))
(+ (length (car (last d))) (plist-get start-p :character))))
(message (prin1-to-string d))
(message (prin1-to-string start-p))
(message (prin1-to-string end-p)))))
|
Hmm, that's an interesting idea. I may try out using |
Here's a proof-of-concept for the idea suggested by @kongds. Seems to work pretty well - the downside is that it's incompatible with |
@astahlman Now that I am actually looking at this, I notice that in the haskell-lsp implementation we already ignore the end position, and work from the range only. So I am in favour of making the end position optional when deleting. |
I am considering using the The idea is to store the values (and converted positions as line/col) from the before change call, and possibly use it in the after change section. This is what I am seeing between in the calls ;; Adding text:
;; lsp-before-change:(start,end )=(33,33)
;; lsp-on-change:(start,end,length)=(33,34,0)
;;
;; Deleting text:
;; lsp-before-change:(start,end)= (19,27)
;; lsp-on-change:(start,end,length)=(19,19,8) The elisp manual warns that you cannot expect these to be neatly bracketed. So I am thinking that we only need to care for the deleting case, and we can do a sanity check that the two start values are the same, and that the If they do not match we can potentially send the change through as the whole buffer, i.e. a @astahlman comments? |
My WIP is here: c989b19 |
@alanz That's an interesting idea. How would it work in the case of substitutions? The warnings you alluded to are concerning, especially this part:
Given that we have no guarantees about when these hooks are invoked, I'm not sure whether the fact that But I suspect that, in practice, the probability of receiving a non-bracketed change where the start positions and range lengths perfectly align would be low enough that this approach may be "good enough". It would be nice if there were support at the protocol level for the client to periodically ask the server, "What's the SHA-1 of your representation of document X?" so that we could periodically verify that the two representations of the document haven't diverged. |
We would still need a fallback method for when two calls to before and after change hooks aren't balanced. |
My proposed fallback is to send a change being the entire buffer. I.e. a full sync. I just need to find time to get back to this, things are running away right now. |
I have updated my branch, seems to work |
See #124 |
It might make sense to make this check optional, if you know you are using a server that does not use the end position. Like |
I think this issue can be closed? |
Yep, thanks! |
While this lowers compatability somewhat, any system that a developer is actively using is expected to have bash installed. This change is required because asdf does not currently support sh, only bash and other more advanced shells. Fixes emacs-lsp#114
The function lsp--point-to-position just go to point and calculate pos of this point.
But when go to end point after kill a line, this point is the new pos to old end point.
For example one buffer has the three lines like this
use kill-line at point 0
so the start pos is (0,0)
but the end pos will be (2,7) should be (0, length of the first line)
The text was updated successfully, but these errors were encountered: