[ty] Added support for "document highlights" language server feature. (#19515)
Some checks are pending
CI / Determine changes (push) Waiting to run
CI / cargo fmt (push) Waiting to run
CI / cargo clippy (push) Blocked by required conditions
CI / cargo test (linux) (push) Blocked by required conditions
CI / cargo test (linux, release) (push) Blocked by required conditions
CI / cargo test (windows) (push) Blocked by required conditions
CI / cargo test (wasm) (push) Blocked by required conditions
CI / cargo build (release) (push) Waiting to run
CI / cargo build (msrv) (push) Blocked by required conditions
CI / cargo fuzz build (push) Blocked by required conditions
CI / fuzz parser (push) Blocked by required conditions
CI / test scripts (push) Blocked by required conditions
CI / mkdocs (push) Waiting to run
CI / ecosystem (push) Blocked by required conditions
CI / Fuzz for new ty panics (push) Blocked by required conditions
CI / cargo shear (push) Blocked by required conditions
CI / python package (push) Waiting to run
CI / pre-commit (push) Waiting to run
CI / formatter instabilities and black similarity (push) Blocked by required conditions
CI / test ruff-lsp (push) Blocked by required conditions
CI / check playground (push) Blocked by required conditions
CI / benchmarks-instrumented (push) Blocked by required conditions
CI / benchmarks-walltime (push) Blocked by required conditions
[ty Playground] Release / publish (push) Waiting to run

This PR adds support for the "document highlights" language server
feature.

This feature allows a client to highlight all instances of a selected
name within a document. Without this feature, editors perform
highlighting based on a simple text match. This adds semantic knowledge.

The implementation of this feature largely overlaps that of the
recently-added "references" feature. This PR refactors the existing
"references.rs" module, separating out the functionality and tests that
are specific to the other language feature into a "goto_references.rs"
module. The "references.rs" module now contains the functionality that
is common to "goto references", "document highlights" and "rename"
(which is not yet implemented).

As part of this PR, I also created a new `ReferenceTarget` type which is
similar to the existing `NavigationTarget` type but better suited for
references. This idea was suggested by @MichaReiser in [this code review
feedback](https://github.com/astral-sh/ruff/pull/19475#discussion_r2224061006)
from a previous PR. Notably, this new type contains a field that
specifies the "kind" of the reference (read, write or other). This
"kind" is needed for the document highlights feature.

Before: all textual instances of `foo` are highlighted
<img width="156" height="126" alt="Screenshot 2025-07-23 at 12 51 09 PM"
src="https://github.com/user-attachments/assets/37ccdb2f-d48a-473d-89d5-8e89cb6c394e"
/>

After: only semantic matches are highlighted
<img width="164" height="157" alt="Screenshot 2025-07-23 at 12 52 05 PM"
src="https://github.com/user-attachments/assets/2efadadd-4691-4815-af04-b031e74c81b7"
/>

---------

Co-authored-by: UnboundVariable <unbound@gmail.com>
This commit is contained in:
UnboundVariable 2025-07-24 13:06:25 -07:00 committed by GitHub
parent d9cab4d242
commit 4bc34b82ef
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 1455 additions and 877 deletions

View file

@ -5,7 +5,7 @@ use lsp_types::Location;
use ruff_db::files::FileRange;
use ruff_db::source::{line_index, source_text};
use ruff_text_size::Ranged;
use ty_ide::NavigationTarget;
use ty_ide::{NavigationTarget, ReferenceTarget};
use ty_project::Db;
pub(crate) trait ToLink {
@ -53,3 +53,37 @@ impl ToLink for NavigationTarget {
})
}
}
impl ToLink for ReferenceTarget {
fn to_location(&self, db: &dyn Db, encoding: PositionEncoding) -> Option<Location> {
self.file_range().to_location(db, encoding)
}
fn to_link(
&self,
db: &dyn Db,
src: Option<FileRange>,
encoding: PositionEncoding,
) -> Option<lsp_types::LocationLink> {
let uri = file_to_url(db, self.file())?;
let source = source_text(db, self.file());
let index = line_index(db, self.file());
let target_range = self.range().to_lsp_range(&source, &index, encoding);
let selection_range = target_range;
let src = src.map(|src| {
let source = source_text(db, src.file());
let index = line_index(db, src.file());
src.range().to_lsp_range(&source, &index, encoding)
});
Some(lsp_types::LocationLink {
target_uri: uri,
target_range,
target_selection_range: selection_range,
origin_selection_range: src,
})
}
}

View file

@ -210,6 +210,7 @@ impl Server {
definition_provider: Some(lsp_types::OneOf::Left(true)),
declaration_provider: Some(DeclarationCapability::Simple(true)),
references_provider: Some(lsp_types::OneOf::Left(true)),
document_highlight_provider: Some(lsp_types::OneOf::Left(true)),
hover_provider: Some(HoverProviderCapability::Simple(true)),
signature_help_provider: Some(SignatureHelpOptions {
trigger_characters: Some(vec!["(".to_string(), ",".to_string()]),

View file

@ -59,6 +59,11 @@ pub(super) fn request(req: server::Request) -> Task {
requests::ReferencesRequestHandler::METHOD => background_document_request_task::<
requests::ReferencesRequestHandler,
>(req, BackgroundSchedule::Worker),
requests::DocumentHighlightRequestHandler::METHOD => background_document_request_task::<
requests::DocumentHighlightRequestHandler,
>(
req, BackgroundSchedule::Worker
),
requests::InlayHintRequestHandler::METHOD => background_document_request_task::<
requests::InlayHintRequestHandler,
>(req, BackgroundSchedule::Worker),

View file

@ -1,11 +1,12 @@
mod completion;
mod diagnostic;
mod doc_highlights;
mod goto_declaration;
mod goto_definition;
mod goto_references;
mod goto_type_definition;
mod hover;
mod inlay_hints;
mod references;
mod semantic_tokens;
mod semantic_tokens_range;
mod shutdown;
@ -14,12 +15,13 @@ mod workspace_diagnostic;
pub(super) use completion::CompletionRequestHandler;
pub(super) use diagnostic::DocumentDiagnosticRequestHandler;
pub(super) use doc_highlights::DocumentHighlightRequestHandler;
pub(super) use goto_declaration::GotoDeclarationRequestHandler;
pub(super) use goto_definition::GotoDefinitionRequestHandler;
pub(super) use goto_references::ReferencesRequestHandler;
pub(super) use goto_type_definition::GotoTypeDefinitionRequestHandler;
pub(super) use hover::HoverRequestHandler;
pub(super) use inlay_hints::InlayHintRequestHandler;
pub(super) use references::ReferencesRequestHandler;
pub(super) use semantic_tokens::SemanticTokensRequestHandler;
pub(super) use semantic_tokens_range::SemanticTokensRangeRequestHandler;
pub(super) use shutdown::ShutdownHandler;

View file

@ -0,0 +1,74 @@
use std::borrow::Cow;
use lsp_types::request::DocumentHighlightRequest;
use lsp_types::{DocumentHighlight, DocumentHighlightKind, DocumentHighlightParams, Url};
use ruff_db::source::{line_index, source_text};
use ty_ide::{ReferenceKind, document_highlights};
use ty_project::ProjectDatabase;
use crate::document::{PositionExt, ToRangeExt};
use crate::server::api::traits::{
BackgroundDocumentRequestHandler, RequestHandler, RetriableRequestHandler,
};
use crate::session::DocumentSnapshot;
use crate::session::client::Client;
pub(crate) struct DocumentHighlightRequestHandler;
impl RequestHandler for DocumentHighlightRequestHandler {
type RequestType = DocumentHighlightRequest;
}
impl BackgroundDocumentRequestHandler for DocumentHighlightRequestHandler {
fn document_url(params: &DocumentHighlightParams) -> Cow<Url> {
Cow::Borrowed(&params.text_document_position_params.text_document.uri)
}
fn run_with_snapshot(
db: &ProjectDatabase,
snapshot: DocumentSnapshot,
_client: &Client,
params: DocumentHighlightParams,
) -> crate::server::Result<Option<Vec<DocumentHighlight>>> {
if snapshot.client_settings().is_language_services_disabled() {
return Ok(None);
}
let Some(file) = snapshot.file(db) else {
return Ok(None);
};
let source = source_text(db, file);
let line_index = line_index(db, file);
let offset = params.text_document_position_params.position.to_text_size(
&source,
&line_index,
snapshot.encoding(),
);
let Some(highlights_result) = document_highlights(db, file, offset) else {
return Ok(None);
};
let highlights: Vec<_> = highlights_result
.into_iter()
.map(|target| {
let range = target
.range()
.to_lsp_range(&source, &line_index, snapshot.encoding());
let kind = match target.kind() {
ReferenceKind::Read => Some(DocumentHighlightKind::READ),
ReferenceKind::Write => Some(DocumentHighlightKind::WRITE),
ReferenceKind::Other => Some(DocumentHighlightKind::TEXT),
};
DocumentHighlight { range, kind }
})
.collect();
Ok(Some(highlights))
}
}
impl RetriableRequestHandler for DocumentHighlightRequestHandler {}

View file

@ -3,7 +3,7 @@ use std::borrow::Cow;
use lsp_types::request::References;
use lsp_types::{Location, ReferenceParams, Url};
use ruff_db::source::{line_index, source_text};
use ty_ide::references;
use ty_ide::goto_references;
use ty_project::ProjectDatabase;
use crate::document::{PositionExt, ToLink};
@ -48,13 +48,12 @@ impl BackgroundDocumentRequestHandler for ReferencesRequestHandler {
let include_declaration = params.context.include_declaration;
let Some(references_result) = references(db, file, offset, include_declaration) else {
let Some(references_result) = goto_references(db, file, offset, include_declaration) else {
return Ok(None);
};
let locations: Vec<_> = references_result
.into_iter()
.flat_map(|ranged| ranged.value.into_iter())
.filter_map(|target| target.to_location(db, snapshot.encoding()))
.collect();