[BUG] LSP client sends rangeless didChange to servers that negotiated incremental sync; Ruby LSP rejects it and keeps the old content

Status Open
Reported on v2.1.296
Maintainer reply None cached
Activity 0 comments · opened Oct 10, 2026

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet
  • [x] This is a single bug report (please file separate reports for different bugs)
  • [x] I am using the latest version of Claude Code

What's Wrong?

With the official ruby-lsp plugin, LSP results stop reflecting edits. After Claude edits a file, documentSymbol still returns the symbols from before the edit.

The cause is the shape of textDocument/didChange. The client sends contentChanges: [{"text": "<whole file>"}] with no range, even though Ruby LSP's initialize result says "textDocumentSync": {"openClose": true, "change": 2} (Incremental). Ruby LSP raises NoMethodError on the rangeless change, and its copy of the document stays at the pre-edit content.

In LSP, the server chooses the sync kind. Incremental means the full content is sent on open and only incremental updates after that. Ruby LSP's maintainers treat rangeless changes as a client bug (Shopify/ruby-lsp#3148). Servers that accept rangeless changes likely hide the problem.

What Should Happen?

Send changes in the form the server negotiated. For Incremental, the client can keep sending the whole file, but with a range that covers the previous content:

{ "range": { "start": { "line": 0, "character": 0 }, "end": <end of the previous text> }, "text": "<whole file>" }

For Full, the current form is correct.

Error Messages/Logs

# client -> server
{"jsonrpc":"2.0","method":"textDocument/didChange","params":{"textDocument":{"uri":"file:///.../a.rb","version":2},"contentChanges":[{"text":"<whole file>"}]}}

# server -> client (window/logMessage)
Error processing textDocument/didChange: .../ruby-lsp-0.26.11/lib/ruby_lsp/document.rb:98:in 'block in RubyLsp::Document#push_edits': undefined method '[]' for nil (NoMethodError)

        start_position = scanner.find_char_position(range[:start])

Steps to Reproduce

  1. gem install ruby-lsp and claude plugin install ruby-lsp@claude-plugins-official. No Ruby LSP add-ons are needed.
  2. In an empty directory, create a.rb:

``ruby
class A
def one; end
end
``

  1. Put a pass-through ruby-lsp first on PATH to record the traffic:

``sh
mkdir bin && cat > bin/ruby-lsp <<EOF
#!/bin/sh
PATH="$PATH"
tee -a "$PWD/client.log" | ruby-lsp "\$@" | tee -a "$PWD/server.log"
EOF
chmod +x bin/ruby-lsp
``

  1. Run:

``sh
PATH="$PWD/bin:$PATH" claude -p "1) LSP documentSymbol on a.rb. 2) Edit a.rb: replace 'def one' with 'def two'. 3) LSP documentSymbol on a.rb." --allowedTools LSP,Edit,Read
``

  1. Check the logs:

``sh
grep -a -o '"contentChanges":\[{"[a-z]*"' client.log
# => "contentChanges":[{"text"
grep -a -c "Error processing textDocument/didChange" server.log
# => 1
``

The second documentSymbol also returns one instead of two. Claude sometimes runs steps 2 and 3 in parallel, so the logs above are the more reliable check.

Claude Model

Not sure / Multiple models

Is this a regression?

I don't know

Last Working Version

_No response_

Claude Code Version

2.1.296 (Claude Code)

Platform

Anthropic API

Operating System

macOS

Terminal/Shell

Other

Additional Information

Ruby LSP 0.26.11, Ruby 4.0.5. Related: #79744 (no didChange at all in interactive mode, auto-closed), #100571.

View original on GitHub ↗