Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 57 additions & 7 deletions crates/ty_ide/src/inlay_hints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -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,
}
}

Expand All @@ -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
Expand All @@ -104,6 +123,10 @@ impl SourceOrderVisitor<'_> for InlayHintVisitor<'_> {

match stmt {
Stmt::Assign(assign) => {
if !self.settings.variable_types {
return;
}
Comment on lines +126 to +128
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems unnecessary to traverse the entire tree only to bail out at the assignment level. Should we add a InlayHintSettings::any_enabled and bail out in the inlay hint request if it returns false to avoid doing all this unnecessary work?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that makes sense.

Copy link
Member Author

@dhruvmanila dhruvmanila Aug 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, I don't think we should add that today because there will be (in the future) multiple positions for which we won't provide a way to disable. I don't think the server should provide configuring every position of inlay hints. So, I'm going to avoid adding this today.

Copy link
Member

@MichaReiser MichaReiser Aug 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you give an example of inlays that the user can't disable and we'd always provide (I'm not disagreeing, just curious)?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, I don't have an example on top of my mind and it might be that it doesn't exists 😅


self.in_assignment = true;
for target in &assign.targets {
self.visit_expr(target);
Expand Down Expand Up @@ -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();

Expand Down Expand Up @@ -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
"
);
}
}
2 changes: 1 addition & 1 deletion crates/ty_ide/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down
2 changes: 1 addition & 1 deletion crates/ty_server/src/server/api/requests/inlay_hints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
33 changes: 32 additions & 1 deletion crates/ty_server/src/session/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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();

Expand All @@ -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.
Expand Down Expand Up @@ -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")]
Expand Down
6 changes: 6 additions & 0 deletions crates/ty_server/src/session/settings.rs
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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>,
}

Expand All @@ -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
}
}
38 changes: 38 additions & 0 deletions crates/ty_server/tests/e2e/inlay_hints.rs
Original file line number Diff line number Diff line change
@@ -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(())
}
48 changes: 40 additions & 8 deletions crates/ty_server/tests/e2e/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
//! [`await_notification`]: TestServer::await_notification

mod initialize;
mod inlay_hints;
mod publish_diagnostics;
mod pull_diagnostics;

Expand All @@ -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;
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down
8 changes: 6 additions & 2 deletions crates/ty_wasm/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down
Loading