Skip to content

Respect server textDocumentSync capability for didChange notifications#194

Merged
shugo merged 2 commits intomainfrom
fix/lsp-respect-text_document_sync
Feb 20, 2026
Merged

Respect server textDocumentSync capability for didChange notifications#194
shugo merged 2 commits intomainfrom
fix/lsp-respect-text_document_sync

Conversation

@shugo
Copy link
Owner

@shugo shugo commented Feb 20, 2026

Solargraph (and other servers) advertise textDocumentSync: 1 (Full), meaning they expect the complete document text on every didChange notification rather than incremental ranges. Sending incremental changes to a Full-sync server leaves it with a stale view of the document, preventing type inference for local variables.

Now lsp_setup_buffer_hooks reads textDocumentSync from server capabilities and sends either a full document or an incremental range accordingly.

Solargraph (and other servers) advertise textDocumentSync: 1 (Full),
meaning they expect the complete document text on every didChange
notification rather than incremental ranges. Sending incremental
changes to a Full-sync server leaves it with a stale view of the
document, preventing type inference for local variables.

Now lsp_setup_buffer_hooks reads textDocumentSync from server
capabilities and sends either a full document or an incremental
range accordingly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds support for respecting the server's textDocumentSync capability in LSP integrations. Previously, the code always sent incremental document changes, which caused issues with servers like Solargraph that expect full document synchronization. The change enables proper type inference for local variables by sending the complete document text when required.

Changes:

  • Modified lsp_setup_buffer_hooks to read and respect the server's textDocumentSync capability
  • Added conditional logic to send either full document text (sync kind 1) or incremental changes (sync kind 2) based on server capabilities
  • Updated comments to document the two sync modes

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

when Hash then sync["change"].to_i
else 2
end

Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new textDocumentSync capability handling logic lacks test coverage. Consider adding tests to verify the behavior for different sync kinds (Full, Incremental, None) and different textDocumentSync formats (Integer vs Hash with "change" field).

Suggested change
# If the server advertises TextDocumentSyncKind::None (0),
# do not send did_change notifications.
next if sync_kind == 0

Copilot uses AI. Check for mistakes.
Comment on lines 262 to 263
when Hash then sync["change"].to_i
else 2
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If sync is a Hash without a "change" key, sync["change"].to_i evaluates to 0 (None), causing the code to incorrectly send incremental changes (else branch) instead of skipping synchronization. Consider using .dig("change") and providing an explicit default: when Hash then sync.dig("change").to_i.nonzero? || 2 or checking for the presence of the key before calling .to_i.

Copilot uses AI. Check for mistakes.
Comment on lines 266 to 300
if sync_kind == 1
# Full document sync (e.g. solargraph)
client.did_change(uri: uri, version: version, text: buffer.to_s)
else
# Deletion: compute old end position from the deleted text
newline_count = old_text.count("\n")
if newline_count == 0
end_line = start_pos[:line]
end_char = start_pos[:character] + lsp_utf16_length(old_text)
# Incremental sync
# Compute start position in LSP coordinates (0-based, UTF-16)
start_pos = lsp_position(buffer, beg_pos)

if old_text.empty?
# Insertion: old range is empty, new text is the inserted content
new_text = buffer.substring(beg_pos, end_pos)
range = { start: start_pos, end: start_pos }
else
end_line = start_pos[:line] + newline_count
last_newline = old_text.rindex("\n")
end_char = lsp_utf16_length(old_text[last_newline + 1..])
# Deletion: compute old end position from the deleted text
newline_count = old_text.count("\n")
if newline_count == 0
end_line = start_pos[:line]
end_char = start_pos[:character] + lsp_utf16_length(old_text)
else
end_line = start_pos[:line] + newline_count
last_newline = old_text.rindex("\n")
end_char = lsp_utf16_length(old_text[last_newline + 1..])
end
range = {
start: start_pos,
end: { line: end_line, character: end_char }
}
new_text = ""
end
range = {
start: start_pos,
end: { line: end_line, character: end_char }
}
new_text = ""
end

client.did_change(
uri: uri, version: version,
text: new_text, range: range
)
client.did_change(
uri: uri, version: version,
text: new_text, range: range
)
end
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code doesn't explicitly handle TextDocumentSyncKind.None (0). If a server specifies sync_kind as 0, the code will still send incremental changes (else branch), which violates the server's expectation. Consider adding a check to skip sending changes when sync_kind is 0: if sync_kind == 0 then next elsif sync_kind == 1 ....

Copilot uses AI. Check for mistakes.
Comment on lines 266 to 268
if sync_kind == 1
# Full document sync (e.g. solargraph)
client.did_change(uri: uri, version: version, text: buffer.to_s)
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For Full document sync, buffer.to_s is called on every character change, which could have performance implications for large files. Consider adding a note in the comment or documentation about this potential performance impact, especially since this is a significant difference from the previous incremental-only behavior.

Copilot uses AI. Check for mistakes.
- Fix Hash without "change" key: use &.to_i || 2 instead of .to_i
  (nil.to_i was 0, incorrectly selecting None instead of Incremental)
- Handle TextDocumentSyncKind::None (0): skip didChange notifications
- Extract lsp_text_document_sync_kind helper for testability
- Add 7 tests covering Integer, Hash, missing key, and absent capability

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@shugo shugo enabled auto-merge February 20, 2026 05:47
@shugo shugo merged commit f7f091a into main Feb 20, 2026
7 checks passed
@shugo shugo deleted the fix/lsp-respect-text_document_sync branch February 20, 2026 05:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants