From 0aa65af279675db6ae24fe426ed7bb3d9563a102 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Efe=20G=C3=B6kdemir?= Date: Mon, 2 Nov 2026 02:28:57 +0300 Subject: [PATCH 1/2] Index declarations on document changes --- lib/ruby_lsp/server.rb | 25 ++++++++++++++++++++++--- test/server_test.rb | 26 +++++++++++++------------- 2 files changed, 35 insertions(+), 16 deletions(-) diff --git a/lib/ruby_lsp/server.rb b/lib/ruby_lsp/server.rb index c700fc6c55..4f46503958 100644 --- a/lib/ruby_lsp/server.rb +++ b/lib/ruby_lsp/server.rb @@ -428,8 +428,22 @@ def text_document_did_close(message) def text_document_did_change(message) params = message[:params] text_document = params[:textDocument] + uri = text_document[:uri] - @store.push_edits(uri: text_document[:uri], edits: params[:contentChanges], version: text_document[:version]) + # Text synchronization notifications are normally not parsed by the main thread. Parse here so we can determine + # whether the edit affects declarations before an editor sends another request. + document = @global_state.synchronize do + @store.push_edits(uri: uri, edits: params[:contentChanges], version: text_document[:version]) + updated_document = @store.get(uri) + updated_document.parse! if updated_document.is_a?(RubyDocument) + updated_document + end + + return unless document.is_a?(RubyDocument) && document.should_index? + + # Populate the same request caches as run_combined_requests while indexing. If the editor asks for one of these + # features after the edit, it can use the results from this traversal instead of traversing the AST again. + compute_combined_requests(uri, document) end #: (Hash[Symbol, untyped] message) -> void @@ -475,6 +489,13 @@ def run_combined_requests(message) return end + compute_combined_requests(uri, document) + + send_message(Result.new(id: message[:id], response: document.cache_get(message[:method]))) + end + + #: (URI::Generic uri, RubyDocument | ERBDocument document) -> void + def compute_combined_requests(uri, document) parse_result = document.parse_result # Run requests for the document @@ -521,8 +542,6 @@ def run_combined_requests(message) .perform, ) document.cache_set("textDocument/inlayHint", inlay_hint.perform) - - send_message(Result.new(id: message[:id], response: document.cache_get(message[:method]))) end alias_method :text_document_document_symbol, :run_combined_requests diff --git a/test/server_test.rb b/test/server_test.rb index 1301c8b59c..1419d04fa0 100644 --- a/test/server_test.rb +++ b/test/server_test.rb @@ -979,7 +979,7 @@ def test_requests_cancelled_during_processing_are_deleted_from_cancelled_request assert_empty(@server.instance_variable_get(:@cancelled_requests)) end - def test_unsaved_changes_are_indexed_when_computing_automatic_features + def test_unsaved_changes_are_indexed_without_combined_requests uri = URI("file:///foo.rb") index = @server.global_state.index @@ -1016,25 +1016,23 @@ def test_unsaved_changes_are_indexed_when_computing_automatic_features }, }) - # Parse the document after it was modified. This occurs automatically when we receive a text document request, to - # avoid parsing the document multiple times, but that depends on request coming in through the STDIN pipe, which - # isn't reproduced here. Parsing manually matches what happens normally - store = @server.instance_variable_get(:@store) - store.get(uri).parse! + # The edit should update the index without waiting for a combined feature request. + entries = index["Foo"] + assert_equal(1, entries.length) - # Trigger the automatic features again + entries = index["bar"] + assert_equal(1, entries.length) + + # A combined request should be served from the results computed during didChange. + index.expects(:handle_change).never @server.process_message({ id: 2, method: "textDocument/documentSymbol", params: { textDocument: { uri: uri } }, }) - # There should still only be one entry for each declaration, but we should have picked up the new ones - entries = index["Foo"] - assert_equal(1, entries.length) - - entries = index["bar"] - assert_equal(1, entries.length) + result = find_message(RubyLsp::Result, id: 2) + assert_equal(1, result.response.length) end def test_ancestors_are_recomputed_even_on_unsaved_changes @@ -1080,6 +1078,8 @@ class Foo }, }) + assert_equal(["Foo::"], index.linearized_ancestors_of("Foo::")) + # Parse the document after it was modified. This occurs automatically when we receive a text document request, to # avoid parsing the document multiple times, but that depends on request coming in through the STDIN pipe, which # isn't reproduced here. Parsing manually matches what happens normally From 76ea5ee8d876006e81f6b68f289f6f240e06f576 Mon Sep 17 00:00:00 2001 From: Efe Date: Tue, 3 Nov 2026 07:57:58 +0300 Subject: [PATCH 2/2] Make server backtrace test independent of checkout name --- test/server_test.rb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/server_test.rb b/test/server_test.rb index 1419d04fa0..4960c52dc4 100644 --- a/test/server_test.rb +++ b/test/server_test.rb @@ -423,10 +423,11 @@ def test_backtrace_is_printed_to_stderr_on_exceptions content = log.params.message assert_match(/boom/, content) + server_path = Regexp.escape(File.expand_path("../lib/ruby_lsp/server.rb", __dir__)) if RUBY_VERSION >= "3.4" - assert_match(%r{ruby-lsp/lib/ruby_lsp/server\.rb:\d+:in 'RubyLsp::Server#process_message'}, content) + assert_match(/#{server_path}:\d+:in 'RubyLsp::Server#process_message'/, content) else - assert_match(%r{ruby-lsp/lib/ruby_lsp/server\.rb:\d+:in `process_message'}, content) + assert_match(/#{server_path}:\d+:in `process_message'/, content) end end