[ty] Add ty.inlayHints.variableTypes server option (#19780)

## Summary

This PR adds a new `ty.inlayHints.variableTypes` server setting to
configure ty to include / exclude inlay hints at variable position.

Currently, we only support inlay hints at this position so this option
basically translates to enabling / disabling inlay hints for now :)

The VS Code extension PR is
https://github.com/astral-sh/ty-vscode/pull/112.

closes: astral-sh/ty#472

## Test Plan

Add E2E tests.
This commit is contained in:
Dhruv Manilawala 2025-08-07 19:16:51 +05:30 committed by GitHub
parent c401a6d86e
commit b22586fa0e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 181 additions and 20 deletions

View file

@ -51,8 +51,13 @@ impl fmt::Display for DisplayInlayHint<'_, '_> {
}
}
pub fn inlay_hints(db: &dyn Db, file: File, range: TextRange) -> Vec<InlayHint<'_>> {
let mut visitor = InlayHintVisitor::new(db, file, range);
pub fn inlay_hints<'db>(
db: &'db dyn Db,
file: File,
range: TextRange,
settings: &InlayHintSettings,
) -> Vec<InlayHint<'db>> {
let mut visitor = InlayHintVisitor::new(db, file, range, settings);
let ast = parsed_module(db, file).load(db);
@ -61,20 +66,34 @@ pub fn inlay_hints(db: &dyn Db, file: File, range: TextRange) -> Vec<InlayHint<'
visitor.hints
}
struct InlayHintVisitor<'db> {
/// Settings to control the behavior of inlay hints.
#[derive(Clone, Default, Debug)]
pub struct InlayHintSettings {
/// Whether to show variable type hints.
///
/// For example, this would enable / disable hints like the ones quoted below:
/// ```python
/// x": Literal[1]" = 1
/// ```
pub variable_types: bool,
}
struct InlayHintVisitor<'a, 'db> {
model: SemanticModel<'db>,
hints: Vec<InlayHint<'db>>,
in_assignment: bool,
range: TextRange,
settings: &'a InlayHintSettings,
}
impl<'db> InlayHintVisitor<'db> {
fn new(db: &'db dyn Db, file: File, range: TextRange) -> Self {
impl<'a, 'db> InlayHintVisitor<'a, 'db> {
fn new(db: &'db dyn Db, file: File, range: TextRange, settings: &'a InlayHintSettings) -> Self {
Self {
model: SemanticModel::new(db, file),
hints: Vec::new(),
in_assignment: false,
range,
settings,
}
}
@ -86,7 +105,7 @@ impl<'db> InlayHintVisitor<'db> {
}
}
impl SourceOrderVisitor<'_> for InlayHintVisitor<'_> {
impl SourceOrderVisitor<'_> for InlayHintVisitor<'_, '_> {
fn enter_node(&mut self, node: AnyNodeRef<'_>) -> TraversalSignal {
if self.range.intersect(node.range()).is_some() {
TraversalSignal::Traverse
@ -104,6 +123,10 @@ impl SourceOrderVisitor<'_> for InlayHintVisitor<'_> {
match stmt {
Stmt::Assign(assign) => {
if !self.settings.variable_types {
return;
}
self.in_assignment = true;
for target in &assign.targets {
self.visit_expr(target);
@ -213,8 +236,21 @@ mod tests {
}
impl InlayHintTest {
/// Returns the inlay hints for the given test case.
///
/// All inlay hints are generated using the applicable settings. Use
/// [`inlay_hints_with_settings`] to generate hints with custom settings.
///
/// [`inlay_hints_with_settings`]: Self::inlay_hints_with_settings
fn inlay_hints(&self) -> String {
let hints = inlay_hints(&self.db, self.file, self.range);
self.inlay_hints_with_settings(&InlayHintSettings {
variable_types: true,
})
}
/// Returns the inlay hints for the given test case with custom settings.
fn inlay_hints_with_settings(&self, settings: &InlayHintSettings) -> String {
let hints = inlay_hints(&self.db, self.file, self.range, settings);
let mut buf = source_text(&self.db, self.file).as_str().to_string();
@ -276,4 +312,18 @@ mod tests {
y = 2
");
}
#[test]
fn disabled_variable_types() {
let test = inlay_hint_test("x = 1");
assert_snapshot!(
test.inlay_hints_with_settings(&InlayHintSettings {
variable_types: false,
}),
@r"
x = 1
"
);
}
}

View file

@ -27,7 +27,7 @@ pub use document_symbols::{document_symbols, document_symbols_with_options};
pub use goto::{goto_declaration, goto_definition, goto_type_definition};
pub use goto_references::goto_references;
pub use hover::hover;
pub use inlay_hints::inlay_hints;
pub use inlay_hints::{InlayHintSettings, inlay_hints};
pub use markup::MarkupKind;
pub use references::ReferencesMode;
pub use rename::{can_rename, rename};

View file

@ -47,7 +47,7 @@ impl BackgroundDocumentRequestHandler for InlayHintRequestHandler {
.range
.to_text_range(&source, &index, snapshot.encoding());
let inlay_hints = inlay_hints(db, file, range);
let inlay_hints = inlay_hints(db, file, range, snapshot.workspace_settings().inlay_hints());
let inlay_hints = inlay_hints
.into_iter()

View file

@ -8,6 +8,7 @@ use serde::{Deserialize, Serialize};
use serde_json::Value;
use ty_combine::Combine;
use ty_ide::InlayHintSettings;
use ty_project::metadata::Options as TyOptions;
use ty_project::metadata::options::ProjectOptionsOverrides;
use ty_project::metadata::value::{RangedValue, RelativePathBuf};
@ -106,6 +107,15 @@ impl ClientOptions {
self
}
#[must_use]
pub fn with_variable_types_inlay_hints(mut self, variable_types: bool) -> Self {
self.workspace
.inlay_hints
.get_or_insert_default()
.variable_types = Some(variable_types);
self
}
#[must_use]
pub fn with_experimental_rename(mut self, enabled: bool) -> Self {
self.global.experimental.get_or_insert_default().rename = Some(enabled);
@ -138,7 +148,7 @@ impl GlobalOptions {
let experimental = self
.experimental
.map(|experimental| ExperimentalSettings {
rename: experimental.rename.unwrap_or_default(),
rename: experimental.rename.unwrap_or(true),
})
.unwrap_or_default();
@ -158,6 +168,9 @@ pub(crate) struct WorkspaceOptions {
/// Whether to disable language services like code completions, hover, etc.
disable_language_services: Option<bool>,
/// Options to configure inlay hints.
inlay_hints: Option<InlayHintOptions>,
/// Information about the currently active Python environment in the VS Code Python extension.
///
/// This is relevant only for VS Code and is populated by the ty VS Code extension.
@ -211,11 +224,29 @@ impl WorkspaceOptions {
WorkspaceSettings {
disable_language_services: self.disable_language_services.unwrap_or_default(),
inlay_hints: self
.inlay_hints
.map(InlayHintOptions::into_settings)
.unwrap_or_default(),
overrides,
}
}
}
#[derive(Clone, Combine, Debug, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
struct InlayHintOptions {
variable_types: Option<bool>,
}
impl InlayHintOptions {
fn into_settings(self) -> InlayHintSettings {
InlayHintSettings {
variable_types: self.variable_types.unwrap_or_default(),
}
}
}
/// Diagnostic mode for the language server.
#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]

View file

@ -1,5 +1,6 @@
use super::options::DiagnosticMode;
use ty_ide::InlayHintSettings;
use ty_project::metadata::options::ProjectOptionsOverrides;
/// Resolved client settings that are shared across all workspaces.
@ -33,6 +34,7 @@ pub(crate) struct ExperimentalSettings {
#[derive(Clone, Default, Debug)]
pub(crate) struct WorkspaceSettings {
pub(super) disable_language_services: bool,
pub(super) inlay_hints: InlayHintSettings,
pub(super) overrides: Option<ProjectOptionsOverrides>,
}
@ -44,4 +46,8 @@ impl WorkspaceSettings {
pub(crate) fn project_options_overrides(&self) -> Option<&ProjectOptionsOverrides> {
self.overrides.as_ref()
}
pub(crate) fn inlay_hints(&self) -> &InlayHintSettings {
&self.inlay_hints
}
}

View file

@ -0,0 +1,38 @@
use anyhow::Result;
use lsp_types::{Position, Range, notification::PublishDiagnostics};
use ruff_db::system::SystemPath;
use ty_server::ClientOptions;
use crate::TestServerBuilder;
/// Tests that disabling variable types inlay hints works correctly.
#[test]
fn variable_inlay_hints_disabled() -> Result<()> {
let workspace_root = SystemPath::new("src");
let foo = SystemPath::new("src/foo.py");
let foo_content = "x = 1";
let mut server = TestServerBuilder::new()?
.with_initialization_options(
ClientOptions::default().with_variable_types_inlay_hints(false),
)
.with_workspace(workspace_root, None)?
.with_file(foo, foo_content)?
.enable_inlay_hints(true)
.build()?
.wait_until_workspaces_are_initialized()?;
server.open_text_document(foo, &foo_content, 1);
let _ = server.await_notification::<PublishDiagnostics>()?;
let hints = server
.inlay_hints_request(foo, Range::new(Position::new(0, 0), Position::new(0, 5)))?
.unwrap();
assert!(
hints.is_empty(),
"Expected no inlay hints, but found: {hints:?}"
);
Ok(())
}

View file

@ -28,6 +28,7 @@
//! [`await_notification`]: TestServer::await_notification
mod initialize;
mod inlay_hints;
mod publish_diagnostics;
mod pull_diagnostics;
@ -48,20 +49,21 @@ use lsp_types::notification::{
Initialized, Notification,
};
use lsp_types::request::{
DocumentDiagnosticRequest, HoverRequest, Initialize, Request, Shutdown, WorkspaceConfiguration,
WorkspaceDiagnosticRequest,
DocumentDiagnosticRequest, HoverRequest, Initialize, InlayHintRequest, Request, Shutdown,
WorkspaceConfiguration, WorkspaceDiagnosticRequest,
};
use lsp_types::{
ClientCapabilities, ConfigurationParams, DiagnosticClientCapabilities,
DidChangeTextDocumentParams, DidChangeWatchedFilesClientCapabilities,
DidChangeWatchedFilesParams, DidCloseTextDocumentParams, DidOpenTextDocumentParams,
DocumentDiagnosticParams, DocumentDiagnosticReportResult, FileEvent, Hover, HoverParams,
InitializeParams, InitializeResult, InitializedParams, NumberOrString, PartialResultParams,
Position, PreviousResultId, PublishDiagnosticsClientCapabilities,
TextDocumentClientCapabilities, TextDocumentContentChangeEvent, TextDocumentIdentifier,
TextDocumentItem, TextDocumentPositionParams, Url, VersionedTextDocumentIdentifier,
WorkDoneProgressParams, WorkspaceClientCapabilities, WorkspaceDiagnosticParams,
WorkspaceDiagnosticReportResult, WorkspaceFolder,
InitializeParams, InitializeResult, InitializedParams, InlayHint, InlayHintClientCapabilities,
InlayHintParams, NumberOrString, PartialResultParams, Position, PreviousResultId,
PublishDiagnosticsClientCapabilities, Range, TextDocumentClientCapabilities,
TextDocumentContentChangeEvent, TextDocumentIdentifier, TextDocumentItem,
TextDocumentPositionParams, Url, VersionedTextDocumentIdentifier, WorkDoneProgressParams,
WorkspaceClientCapabilities, WorkspaceDiagnosticParams, WorkspaceDiagnosticReportResult,
WorkspaceFolder,
};
use ruff_db::system::{OsSystem, SystemPath, SystemPathBuf, TestSystem};
use rustc_hash::FxHashMap;
@ -725,6 +727,23 @@ impl TestServer {
let id = self.send_request::<HoverRequest>(params);
self.await_response::<HoverRequest>(&id)
}
/// Sends a `textDocument/inlayHint` request for the document at the given path and range.
pub(crate) fn inlay_hints_request(
&mut self,
path: impl AsRef<SystemPath>,
range: Range,
) -> Result<Option<Vec<InlayHint>>> {
let params = InlayHintParams {
text_document: TextDocumentIdentifier {
uri: self.file_uri(path),
},
range,
work_done_progress_params: WorkDoneProgressParams::default(),
};
let id = self.send_request::<InlayHintRequest>(params);
self.await_response::<InlayHintRequest>(&id)
}
}
impl fmt::Debug for TestServer {
@ -908,6 +927,19 @@ impl TestServerBuilder {
self
}
/// Enable or disable inlay hints capability
pub(crate) fn enable_inlay_hints(mut self, enabled: bool) -> Self {
self.client_capabilities
.text_document
.get_or_insert_default()
.inlay_hint = if enabled {
Some(InlayHintClientCapabilities::default())
} else {
None
};
self
}
/// Enable or disable file watching capability
#[expect(dead_code)]
pub(crate) fn enable_did_change_watched_files(mut self, enabled: bool) -> Self {

View file

@ -16,8 +16,8 @@ use ruff_python_formatter::formatted_file;
use ruff_source_file::{LineIndex, OneIndexed, SourceLocation};
use ruff_text_size::{Ranged, TextSize};
use ty_ide::{
MarkupKind, RangedValue, document_highlights, goto_declaration, goto_definition,
goto_references, goto_type_definition, hover, inlay_hints,
InlayHintSettings, MarkupKind, RangedValue, document_highlights, goto_declaration,
goto_definition, goto_references, goto_type_definition, hover, inlay_hints,
};
use ty_ide::{NavigationTargets, signature_help};
use ty_project::metadata::options::Options;
@ -435,6 +435,10 @@ impl Workspace {
&self.db,
file_id.file,
range.to_text_range(&index, &source, self.position_encoding)?,
// TODO: Provide a way to configure this
&InlayHintSettings {
variable_types: true,
},
);
Ok(result