[ty] Implemented support for "selection range" language server feature (#19567)

This PR adds support for the "selection range" language server feature.
This feature was recently requested by a ty user in [this feature
request](https://github.com/astral-sh/ty/issues/882).

This feature allows a client to implement "smart selection expansion"
based on the structure of the parse tree. For example, if you type
"shift-ctrl-right-arrow" in VS Code, the current selection will be
expanded to include the parent AST node. Conversely,
"shift-ctrl-left-arrow" shrinks the selection.

We will probably need to tune the granularity of selection expansion
based on user feedback. The initial implementation includes most AST
nodes, but users may find this to be too fine-grained. We have the
option of skipping some AST nodes that are not as meaningful when
editing code.

Co-authored-by: UnboundVariable <unbound@gmail.com>
This commit is contained in:
UnboundVariable 2025-07-26 09:08:36 -07:00 committed by GitHub
parent e867830848
commit 738246627f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 391 additions and 3 deletions

View file

@ -7,9 +7,10 @@ use lsp_server::Connection;
use lsp_types::{
ClientCapabilities, DeclarationCapability, DiagnosticOptions, DiagnosticServerCapabilities,
HoverProviderCapability, InitializeParams, InlayHintOptions, InlayHintServerCapabilities,
MessageType, SemanticTokensLegend, SemanticTokensOptions, SemanticTokensServerCapabilities,
ServerCapabilities, SignatureHelpOptions, TextDocumentSyncCapability, TextDocumentSyncKind,
TextDocumentSyncOptions, TypeDefinitionProviderCapability, Url, WorkDoneProgressOptions,
MessageType, SelectionRangeProviderCapability, SemanticTokensLegend, SemanticTokensOptions,
SemanticTokensServerCapabilities, ServerCapabilities, SignatureHelpOptions,
TextDocumentSyncCapability, TextDocumentSyncKind, TextDocumentSyncOptions,
TypeDefinitionProviderCapability, Url, WorkDoneProgressOptions,
};
use ruff_db::system::System;
use std::num::NonZeroUsize;
@ -241,6 +242,7 @@ impl Server {
trigger_characters: Some(vec!['.'.to_string()]),
..Default::default()
}),
selection_range_provider: Some(SelectionRangeProviderCapability::Simple(true)),
document_symbol_provider: Some(lsp_types::OneOf::Left(true)),
workspace_symbol_provider: Some(lsp_types::OneOf::Left(true)),
..Default::default()

View file

@ -84,6 +84,9 @@ pub(super) fn request(req: server::Request) -> Task {
>(
req, BackgroundSchedule::LatencySensitive
),
requests::SelectionRangeRequestHandler::METHOD => background_document_request_task::<
requests::SelectionRangeRequestHandler,
>(req, BackgroundSchedule::Worker),
requests::DocumentSymbolRequestHandler::METHOD => background_document_request_task::<
requests::DocumentSymbolRequestHandler,
>(req, BackgroundSchedule::Worker),

View file

@ -8,6 +8,7 @@ mod goto_references;
mod goto_type_definition;
mod hover;
mod inlay_hints;
mod selection_range;
mod semantic_tokens;
mod semantic_tokens_range;
mod shutdown;
@ -25,6 +26,7 @@ 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 selection_range::SelectionRangeRequestHandler;
pub(super) use semantic_tokens::SemanticTokensRequestHandler;
pub(super) use semantic_tokens_range::SemanticTokensRangeRequestHandler;
pub(super) use shutdown::ShutdownHandler;

View file

@ -0,0 +1,73 @@
use std::borrow::Cow;
use lsp_types::request::SelectionRangeRequest;
use lsp_types::{SelectionRange as LspSelectionRange, SelectionRangeParams, Url};
use ruff_db::source::{line_index, source_text};
use ty_ide::selection_range;
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 SelectionRangeRequestHandler;
impl RequestHandler for SelectionRangeRequestHandler {
type RequestType = SelectionRangeRequest;
}
impl BackgroundDocumentRequestHandler for SelectionRangeRequestHandler {
fn document_url(params: &SelectionRangeParams) -> Cow<Url> {
Cow::Borrowed(&params.text_document.uri)
}
fn run_with_snapshot(
db: &ProjectDatabase,
snapshot: DocumentSnapshot,
_client: &Client,
params: SelectionRangeParams,
) -> crate::server::Result<Option<Vec<LspSelectionRange>>> {
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 mut results = Vec::new();
for position in params.positions {
let offset = position.to_text_size(&source, &line_index, snapshot.encoding());
let ranges = selection_range(db, file, offset);
if !ranges.is_empty() {
// Convert ranges to nested LSP SelectionRange structure
let mut lsp_range = None;
for &range in &ranges {
lsp_range = Some(LspSelectionRange {
range: range.to_lsp_range(&source, &line_index, snapshot.encoding()),
parent: lsp_range.map(Box::new),
});
}
if let Some(range) = lsp_range {
results.push(range);
}
}
}
if results.is_empty() {
Ok(None)
} else {
Ok(Some(results))
}
}
}
impl RetriableRequestHandler for SelectionRangeRequestHandler {}