Skip to content
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
51f1bc4
Initial progress in playground
MatthewMckee4 Apr 4, 2025
9a98afb
Remove redundant calls
MatthewMckee4 Apr 4, 2025
b110361
Fix build and filter some more types
MatthewMckee4 Apr 4, 2025
978cc48
Refactory and add more filter logic
MatthewMckee4 Apr 4, 2025
cb898fe
Fix build
MatthewMckee4 Apr 4, 2025
41f583e
Add knot server functionality for inlay hints
MatthewMckee4 Apr 5, 2025
0da803d
Add InlayHintVisitor to visit only the statements we want
MatthewMckee4 Apr 5, 2025
135d8a6
Add tests
MatthewMckee4 Apr 5, 2025
00eeaf5
Merge branch 'main' into inlay-hints
MatthewMckee4 Apr 5, 2025
bdb75b1
Fix build
MatthewMckee4 Apr 5, 2025
52106bc
Revert "Fix build"
MatthewMckee4 Apr 5, 2025
a135a6d
Fix build
MatthewMckee4 Apr 5, 2025
2d6b3d5
Fix build
MatthewMckee4 Apr 5, 2025
fad5abb
Add correct tuple inlay hints and update tests
MatthewMckee4 Apr 6, 2025
cef34a5
Add option to opt in to inlay hints
MatthewMckee4 Apr 6, 2025
c1c5434
run generate-all
MatthewMckee4 Apr 6, 2025
08cfce7
Update inlay_hints.rs
MatthewMckee4 Apr 7, 2025
d1d071a
Merge branch 'main' into inlay-hints
MatthewMckee4 Apr 7, 2025
313f76f
Apply changes as per review
MatthewMckee4 Apr 7, 2025
350ff11
run generate-all
MatthewMckee4 Apr 7, 2025
85377eb
Changes per review
MatthewMckee4 Apr 9, 2025
808a03a
Merge branch 'main' into inlay-hints
MatthewMckee4 Apr 9, 2025
b901496
Fix playground
MatthewMckee4 Apr 9, 2025
b2bc7d2
Update per review
MatthewMckee4 Apr 9, 2025
9e66656
Merge branch 'main' into inlay-hints
MatthewMckee4 Apr 9, 2025
e438ffa
Simplify tests
MichaReiser Apr 10, 2025
70fb3e1
Show hint for all `ExprName::Store` locations
MichaReiser Apr 10, 2025
59f3c7f
Extract helper in wasm code
MichaReiser Apr 10, 2025
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
193 changes: 193 additions & 0 deletions crates/red_knot_ide/src/inlay_hints.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
use crate::{Db, RangedValue};
use red_knot_python_semantic::types::Type;
use red_knot_python_semantic::{HasType, SemanticModel};
use ruff_db::files::{File, FileRange};
use ruff_db::parsed::parsed_module;
use ruff_python_ast::visitor::source_order::{self, SourceOrderVisitor};
use ruff_python_ast::{Expr, Stmt};
use ruff_text_size::{Ranged, TextRange};
use std::fmt;
use std::fmt::Formatter;

#[derive(Debug, Clone, Eq, PartialEq)]
pub enum InlayHintContent<'db> {
AssignStatement(Type<'db>),
FunctionReturnType(Type<'db>),
}

impl<'db> InlayHintContent<'db> {
pub const fn display(&self, db: &'db dyn Db) -> DisplayInlayHint<'_, 'db> {
DisplayInlayHint { db, hint: self }
}
}

pub struct DisplayInlayHint<'a, 'db> {
db: &'db dyn Db,
hint: &'a InlayHintContent<'db>,
}

impl fmt::Display for DisplayInlayHint<'_, '_> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self.hint {
InlayHintContent::AssignStatement(ty) => {
write!(f, ": {}", ty.display(self.db.upcast()))
}
InlayHintContent::FunctionReturnType(ty) => {
write!(f, " -> {}", ty.display(self.db.upcast()))
}
}
}
}

pub fn inlay_hints(db: &dyn Db, file: File) -> Vec<RangedValue<InlayHintContent<'_>>> {
let mut visitor = InlayHintVisitor::new(db, file);

let ast = parsed_module(db.upcast(), file);

visitor.visit_body(ast.suite());

let hints = visitor.hints().clone();

hints
}
Comment thread
MatthewMckee4 marked this conversation as resolved.
Outdated

struct InlayHintVisitor<'db> {
Comment thread
MatthewMckee4 marked this conversation as resolved.
model: SemanticModel<'db>,
file: File,
hints: Vec<RangedValue<InlayHintContent<'db>>>,
}

impl<'db> InlayHintVisitor<'db> {
fn new(db: &'db dyn Db, file: File) -> Self {
Self {
model: SemanticModel::new(db.upcast(), file),
file,
hints: Vec::new(),
}
}

fn hints(&self) -> &Vec<RangedValue<InlayHintContent<'db>>> {
&self.hints
}

fn add_hint(&mut self, range: TextRange, ty: Type<'db>) {
self.hints.push(RangedValue {
range: FileRange::new(self.file, range),
value: InlayHintContent::AssignStatement(ty),
});
}
}

impl SourceOrderVisitor<'_> for InlayHintVisitor<'_> {
fn visit_stmt(&mut self, stmt: &Stmt) {
match stmt {
Stmt::Assign(assign) => {
let ty = assign.value.inferred_type(&self.model);
for target in &assign.targets {
match target {
Expr::Tuple(tuple) => {
for element in &tuple.elts {
let element_ty = element.inferred_type(&self.model);
self.add_hint(element.range(), element_ty);
}
}
Comment thread
MatthewMckee4 marked this conversation as resolved.
Outdated
_ => {
self.add_hint(target.range(), ty);
}
Comment thread
MatthewMckee4 marked this conversation as resolved.
Outdated
}
}
return;
}
// TODO
Stmt::FunctionDef(_) => {}
Stmt::For(_) => {}
Stmt::Expr(_) => {}
_ => {}
}

source_order::walk_stmt(self, stmt);
}
}

#[cfg(test)]
mod tests {
use super::*;

use red_knot_python_semantic::types::StringLiteralType;
use ruff_db::files::{system_path_to_file, File};
use ruff_db::system::DbWithWritableSystem as _;
use ruff_text_size::TextSize;

use crate::db::tests::TestDb;

struct TestCase {
db: TestDb,
file: File,
}

fn test_case(content: impl AsRef<str>) -> TestCase {
let mut db = TestDb::new();
db.write_file("test.py", content).unwrap();

let file = system_path_to_file(&db, "test.py").unwrap();

TestCase { db, file }
}

#[test]
fn test_assign_statement() {
let test_case = test_case("x = 1");
let hints = get_inlay_hints(&test_case.db, test_case.file);
assert_eq!(hints.len(), 1);
assert_eq!(
hints[0].value,
InlayHintContent::AssignStatement(Type::IntLiteral(1))
);
assert_eq!(
hints[0].range,
FileRange::new(
test_case.file,
TextRange::new(TextSize::from(0), TextSize::from(1))
)
);
Comment thread
MatthewMckee4 marked this conversation as resolved.
Outdated
}

#[test]
fn test_tuple_assignment() {
let test_case = test_case("x, y = (1, 'abc')");
let hints = get_inlay_hints(&test_case.db, test_case.file);
assert_eq!(hints.len(), 2);
assert_eq!(
hints[0].value,
InlayHintContent::AssignStatement(Type::IntLiteral(1))
);
assert_eq!(
hints[1].value,
InlayHintContent::AssignStatement(Type::StringLiteral(StringLiteralType::new(
&test_case.db,
"abc"
)))
);
assert_eq!(
hints[0].range,
FileRange::new(
test_case.file,
TextRange::new(TextSize::from(0), TextSize::from(1))
)
);
assert_eq!(
hints[1].range,
FileRange::new(
test_case.file,
TextRange::new(TextSize::from(3), TextSize::from(4))
)
);
}

#[test]
fn test_assign_statement_with_type_annotation() {
let test_case = test_case("x: int = 1");
let hints = get_inlay_hints(&test_case.db, test_case.file);
assert_eq!(hints.len(), 0);
}
}
2 changes: 2 additions & 0 deletions crates/red_knot_ide/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ mod db;
mod find_node;
mod goto;
mod hover;
mod inlay_hints;
mod markup;

pub use db::Db;
pub use goto::goto_type_definition;
pub use hover::hover;
pub use inlay_hints::get_inlay_hints;
pub use markup::MarkupKind;

use rustc_hash::FxHashSet;
Expand Down
14 changes: 14 additions & 0 deletions crates/red_knot_project/src/metadata/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ pub struct Options {

#[serde(skip_serializing_if = "Option::is_none")]
pub terminal: Option<TerminalOptions>,

#[serde(skip_serializing_if = "Option::is_none")]
pub editor: Option<EditorOptions>,
}

impl Options {
Expand Down Expand Up @@ -407,3 +410,14 @@ impl OptionDiagnostic {
}
}
}

#[derive(Debug, Default, Clone, Eq, PartialEq, Combine, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct EditorOptions {
/// Whether to show inlay hints.
///
/// Defaults to `false`.
#[serde(skip_serializing_if = "Option::is_none")]
pub inlay_hints: Option<bool>,
}
8 changes: 6 additions & 2 deletions crates/red_knot_server/src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@ use std::panic::PanicInfo;
use lsp_server::Message;
use lsp_types::{
ClientCapabilities, DiagnosticOptions, DiagnosticServerCapabilities, HoverProviderCapability,
MessageType, ServerCapabilities, TextDocumentSyncCapability, TextDocumentSyncKind,
TextDocumentSyncOptions, TypeDefinitionProviderCapability, Url,
InlayHintOptions, InlayHintServerCapabilities, MessageType, ServerCapabilities,
TextDocumentSyncCapability, TextDocumentSyncKind, TextDocumentSyncOptions,
TypeDefinitionProviderCapability, Url,
};

use self::connection::{Connection, ConnectionInitializer};
Expand Down Expand Up @@ -222,6 +223,9 @@ impl Server {
)),
type_definition_provider: Some(TypeDefinitionProviderCapability::Simple(true)),
hover_provider: Some(HoverProviderCapability::Simple(true)),
inlay_hint_provider: Some(lsp_types::OneOf::Right(
InlayHintServerCapabilities::Options(InlayHintOptions::default()),
)),
..Default::default()
}
}
Expand Down
5 changes: 5 additions & 0 deletions crates/red_knot_server/src/server/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ pub(super) fn request<'a>(req: server::Request) -> Task<'a> {
request::HoverRequestHandler::METHOD => background_request_task::<
request::HoverRequestHandler,
>(req, BackgroundSchedule::LatencySensitive),
request::InlayHintRequestHandler::METHOD => background_request_task::<
request::InlayHintRequestHandler,
>(
req, BackgroundSchedule::LatencySensitive
Comment thread
MatthewMckee4 marked this conversation as resolved.
Outdated
),

method => {
tracing::warn!("Received request {method} which does not have a handler");
Expand Down
2 changes: 2 additions & 0 deletions crates/red_knot_server/src/server/api/requests.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
mod diagnostic;
mod goto_type_definition;
mod hover;
mod inlay_hints;

pub(super) use diagnostic::DocumentDiagnosticRequestHandler;
pub(super) use goto_type_definition::GotoTypeDefinitionRequestHandler;
pub(super) use hover::HoverRequestHandler;
pub(super) use inlay_hints::InlayHintRequestHandler;
78 changes: 78 additions & 0 deletions crates/red_knot_server/src/server/api/requests/inlay_hints.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
use std::borrow::Cow;

use crate::server::api::traits::{BackgroundDocumentRequestHandler, RequestHandler};
use crate::server::client::Notifier;
use crate::DocumentSnapshot;
use lsp_types::request::InlayHintRequest;
use lsp_types::{InlayHintParams, Url};
use red_knot_ide::get_inlay_hints;
use red_knot_project::Db;
use red_knot_project::ProjectDatabase;
use ruff_db::source::{line_index, source_text};
use ruff_text_size::Ranged;

pub(crate) struct InlayHintRequestHandler;

impl RequestHandler for InlayHintRequestHandler {
type RequestType = InlayHintRequest;
}

impl BackgroundDocumentRequestHandler for InlayHintRequestHandler {
fn document_url(params: &InlayHintParams) -> Cow<Url> {
Cow::Borrowed(&params.text_document.uri)
}

fn run_with_snapshot(
snapshot: DocumentSnapshot,
db: ProjectDatabase,
_notifier: Notifier,
params: InlayHintParams,
) -> crate::server::Result<Option<Vec<lsp_types::InlayHint>>> {
let Some(file) = snapshot.file(&db) else {
tracing::debug!("Failed to resolve file for {:?}", params);
return Ok(None);
};

let editor_options = db
.project()
.metadata(&db)
.options()
.editor
.clone()
.unwrap_or_default();
Comment thread
MatthewMckee4 marked this conversation as resolved.
Outdated

if !editor_options.inlay_hints.unwrap_or(false) {
return Ok(None);
}

let inlay_hints = get_inlay_hints(&db, file);
Comment thread
MatthewMckee4 marked this conversation as resolved.
Outdated

let index = line_index(&db, file);
let source = source_text(&db, file);

let inlay_hints = inlay_hints
.into_iter()
.map(|hint| {
let end = index.source_location(hint.range.range().end(), &source);

lsp_types::InlayHint {
position: lsp_types::Position {
line: u32::try_from(end.row.to_zero_indexed())
.expect("row usize fits in u32"),
character: u32::try_from(end.column.to_zero_indexed())
.expect("character usize fits in u32"),
},
label: lsp_types::InlayHintLabel::String(hint.display(&db).to_string()),
Comment thread
MatthewMckee4 marked this conversation as resolved.
Outdated
kind: None,
Comment thread
MatthewMckee4 marked this conversation as resolved.
Outdated
tooltip: None,
padding_left: None,
padding_right: None,
data: None,
text_edits: None,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Nit: Not for this PR but we want to use the InlayHintResolveRequest to lazily resolve the tooltip and text edits. It may be worth adding a comment, see https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#inlayHint_resolve

}
})
.collect();

Ok(Some(inlay_hints))
}
}
Loading