diff --git a/crates/ty_ide/src/inlay_hints.rs b/crates/ty_ide/src/inlay_hints.rs index aa1e170709..af92d3440c 100644 --- a/crates/ty_ide/src/inlay_hints.rs +++ b/crates/ty_ide/src/inlay_hints.rs @@ -51,8 +51,13 @@ impl fmt::Display for DisplayInlayHint<'_, '_> { } } -pub fn inlay_hints(db: &dyn Db, file: File, range: TextRange) -> Vec> { - let mut visitor = InlayHintVisitor::new(db, file, range); +pub fn inlay_hints<'db>( + db: &'db dyn Db, + file: File, + range: TextRange, + settings: &InlayHintSettings, +) -> Vec> { + 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 { +/// 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>, 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 + " + ); + } } diff --git a/crates/ty_ide/src/lib.rs b/crates/ty_ide/src/lib.rs index 71ffe5bc8d..1d3a8f7b68 100644 --- a/crates/ty_ide/src/lib.rs +++ b/crates/ty_ide/src/lib.rs @@ -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}; diff --git a/crates/ty_server/src/server/api/requests/inlay_hints.rs b/crates/ty_server/src/server/api/requests/inlay_hints.rs index 23f1b13c13..ece93110bb 100644 --- a/crates/ty_server/src/server/api/requests/inlay_hints.rs +++ b/crates/ty_server/src/server/api/requests/inlay_hints.rs @@ -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() diff --git a/crates/ty_server/src/session/options.rs b/crates/ty_server/src/session/options.rs index 57e52373c3..dc874215f1 100644 --- a/crates/ty_server/src/session/options.rs +++ b/crates/ty_server/src/session/options.rs @@ -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, + /// Options to configure inlay hints. + inlay_hints: Option, + /// 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, +} + +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")] diff --git a/crates/ty_server/src/session/settings.rs b/crates/ty_server/src/session/settings.rs index ac9e3dd1c7..9f54c7069d 100644 --- a/crates/ty_server/src/session/settings.rs +++ b/crates/ty_server/src/session/settings.rs @@ -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, } @@ -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 + } } diff --git a/crates/ty_server/tests/e2e/inlay_hints.rs b/crates/ty_server/tests/e2e/inlay_hints.rs new file mode 100644 index 0000000000..b95c1bdf5b --- /dev/null +++ b/crates/ty_server/tests/e2e/inlay_hints.rs @@ -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::()?; + + 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(()) +} diff --git a/crates/ty_server/tests/e2e/main.rs b/crates/ty_server/tests/e2e/main.rs index 97cd72b8a4..5963755f72 100644 --- a/crates/ty_server/tests/e2e/main.rs +++ b/crates/ty_server/tests/e2e/main.rs @@ -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::(params); self.await_response::(&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, + range: Range, + ) -> Result>> { + let params = InlayHintParams { + text_document: TextDocumentIdentifier { + uri: self.file_uri(path), + }, + range, + work_done_progress_params: WorkDoneProgressParams::default(), + }; + let id = self.send_request::(params); + self.await_response::(&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 { diff --git a/crates/ty_wasm/src/lib.rs b/crates/ty_wasm/src/lib.rs index 9f4051f2ae..817cabdb17 100644 --- a/crates/ty_wasm/src/lib.rs +++ b/crates/ty_wasm/src/lib.rs @@ -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