Skip to content

Do not index documents excluded from indexing when they are opened - #4200

Open
rience wants to merge 1 commit into
Shopify:mainfrom
rience:honour-excluded-patterns-when-opening-documents
Open

Do not index documents excluded from indexing when they are opened#4200
rience wants to merge 1 commit into
Shopify:mainfrom
rience:honour-excluded-patterns-when-opening-documents

Conversation

@rience

@rience rience commented Sep 6, 2026

Copy link
Copy Markdown

Motivation

excludedPatterns is applied only while collecting indexable_uris. When the editor opens a document, the combined requests path indexes it unconditionally — RubyDocument#should_index? returns true for any freshly opened document and never consults the index configuration — so an excluded file lands in the index as soon as somebody looks at it.

That is surprising on its own, but it is most visible in multi-root workspaces where one folder contains another, which is the normal shape of a monorepo. Both the parent folder and the nested folder spawn a language server, both match the document selector for the same file (${fsPath}/**/*), and both index it on open. Every request that reads the index then answers twice, and the editor merges the responses, so Go to Definition opens a peek view titled Definitions (2) listing the same location twice.

Concretely, with a workspace whose folders are the repo root plus every app and component nested under it, opening apps/billing/app/models/application_record.rb logs this, 5ms apart:

[info] (root) Determined that document should be indexed: file:///…/apps/billing/app/models/application_record.rb
[info] (billing) Determined that document should be indexed: file:///…/apps/billing/app/models/application_record.rb

and Go to Definition on any constant in that file offers two identical locations. Configuring the root workspace with

"rubyLsp.indexing": { "excludedPatterns": ["apps/**/*", "components/**/*"] }

correctly shrinks the root server's initial index (7,300 files down to 2,083 in the workspace I reproduced this on), but has no effect on documents once they are opened, so the duplicates come back for exactly the files you are working in.

This is related to #3639, but distinct: that issue is about one index gaining duplicate entries, whereas this is two servers each holding one entry for the same declaration.

Implementation

  • Extract the exclusion check out of indexable_uris into a public Configuration#excluded?(path), so there is one definition of "excluded" rather than a check inlined in the collection loop.
  • Consult it before indexing an opened document in the combined requests path.
  • Memoize the absolutized exclusion patterns, since exclusion is now evaluated once per indexable file rather than once per indexable_uris call, and invalidate that memo when either the patterns (apply_config) or the workspace path change. @bundle_path moves to the initializer for the same reason.

excluded? deliberately only accounts for the exclusion patterns. A file that is merely not covered by the inclusion patterns is not reported as excluded, which keeps the semantics of the didOpen check the same as the semantics of indexable_uris.

Automated Tests

  • Configuration#excluded?: relative patterns, absolute patterns, nil paths, and memo invalidation when workspace_path changes.
  • Server: opening a document that matches excludedPatterns and then firing the document symbol request the editor sends on open leaves the index empty. Verified that this test fails without the server.rb change and passes with it.

bundle exec rake test (18,542 runs, 0 failures, 0 errors), bundle exec rake test:indexer (322 runs, 0 failures), bundle exec rubocop and bundle exec srb tc are all clean locally.

Manual verification

Reproduced against a fixture with the same shape as the workspace this came from — a folder rooted at the repo root with excludedPatterns: ["apps/**/*"], containing apps/billing/app/models/{application_record,invoice}.rb — driving the server through didOpen, the documentSymbol request the editor sends on open, and then textDocument/definition on the superclass:

root server indexes ApplicationRecord definitions returned
main yes 1
this branch no 0

The location the root server returned on main is identical to the one the nested billing server returns, which is what the editor renders as two entries. Note that reproducing this requires the file declaring the constant to be open, since that is what gets force-indexed — opening only the referencing file is not enough.

I would still be happy to be redirected if you would rather fix this on the extension side by narrowing each client's document selector so that the closest workspace folder owns a file. That would also cover multi-root users who have not configured excludedPatterns at all, which this change does not.

`excludedPatterns` is only applied while collecting `indexable_uris`. When
the editor opens a document, the combined requests path indexes it
unconditionally, so an excluded file still ends up in the index as soon as
someone looks at it.

This is visible in multi-root workspaces where one folder contains another,
which is common in monorepos. Both the parent and the nested folder spawn a
server, both match the document selector for the same file, and both index it
on open. Every request that reads the index then returns the same result
twice: `Definitions (2)`, listing one identical location per server.

Extract the exclusion check out of `indexable_uris` into
`Configuration#excluded?` and consult it before indexing an opened document.
The absolute exclusion patterns are memoized, since exclusion is now checked
once per indexable file, and invalidated when either the patterns or the
workspace path change.
@rience

rience commented Sep 6, 2026

Copy link
Copy Markdown
Author

I have signed the CLA!

@rience
rience marked this pull request as ready for review September 6, 2026 07:54
@rience
rience requested a review from a team as a code owner September 6, 2026 07:54
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.

1 participant