Skip to content

Commit a4ba10f

Browse files
authored
[red-knot] Add basic on-hover to playground and LSP (#17057)
## Summary Implement a very basic hover in the playground and LSP. It's basic, because it only shows the type on-hover. Most other LSPs also show: * The signature of the symbol beneath the cursor. E.g. `class Test(a:int, b:int)` (we want something like https://github.com/microsoft/pyright/blob/54f7da25f9c2b6253803602048b04fe0ccb13430/packages/pyright-internal/src/analyzer/typeEvaluator.ts#L21929-L22129) * The symbols' documentation * Do more fancy markdown rendering I decided to defer these features for now because it requires new semantic APIs (similar to *goto definition*), and investing in fancy rendering only makes sense once we have the relevant data. Closes [#16826](#16826) ## Test Plan https://github.com/user-attachments/assets/044aeee4-58ad-4d4e-9e26-ac2a712026be https://github.com/user-attachments/assets/4a1f4004-2982-4cf2-9dfd-cb8b84ff2ecb
1 parent bf03068 commit a4ba10f

File tree

15 files changed

+998
-157
lines changed

15 files changed

+998
-157
lines changed

crates/red_knot_ide/src/goto.rs

+73-136
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use crate::find_node::covering_node;
22
use crate::{Db, HasNavigationTargets, NavigationTargets, RangedValue};
3+
use red_knot_python_semantic::types::Type;
34
use red_knot_python_semantic::{HasType, SemanticModel};
45
use ruff_db::files::{File, FileRange};
56
use ruff_db::parsed::{parsed_module, ParsedModule};
@@ -16,31 +17,7 @@ pub fn goto_type_definition(
1617
let goto_target = find_goto_target(parsed, offset)?;
1718

1819
let model = SemanticModel::new(db.upcast(), file);
19-
20-
let ty = match goto_target {
21-
GotoTarget::Expression(expression) => expression.inferred_type(&model),
22-
GotoTarget::FunctionDef(function) => function.inferred_type(&model),
23-
GotoTarget::ClassDef(class) => class.inferred_type(&model),
24-
GotoTarget::Parameter(parameter) => parameter.inferred_type(&model),
25-
GotoTarget::Alias(alias) => alias.inferred_type(&model),
26-
GotoTarget::ExceptVariable(except) => except.inferred_type(&model),
27-
GotoTarget::KeywordArgument(argument) => {
28-
// TODO: Pyright resolves the declared type of the matching parameter. This seems more accurate
29-
// than using the inferred value.
30-
argument.value.inferred_type(&model)
31-
}
32-
// TODO: Support identifier targets
33-
GotoTarget::PatternMatchRest(_)
34-
| GotoTarget::PatternKeywordArgument(_)
35-
| GotoTarget::PatternMatchStarName(_)
36-
| GotoTarget::PatternMatchAsName(_)
37-
| GotoTarget::ImportedModule(_)
38-
| GotoTarget::TypeParamTypeVarName(_)
39-
| GotoTarget::TypeParamParamSpecName(_)
40-
| GotoTarget::TypeParamTypeVarTupleName(_)
41-
| GotoTarget::NonLocal { .. }
42-
| GotoTarget::Globals { .. } => return None,
43-
};
20+
let ty = goto_target.inferred_type(&model)?;
4421

4522
tracing::debug!(
4623
"Inferred type of covering node is {}",
@@ -149,6 +126,37 @@ pub(crate) enum GotoTarget<'a> {
149126
},
150127
}
151128

129+
impl<'db> GotoTarget<'db> {
130+
pub(crate) fn inferred_type(self, model: &SemanticModel<'db>) -> Option<Type<'db>> {
131+
let ty = match self {
132+
GotoTarget::Expression(expression) => expression.inferred_type(model),
133+
GotoTarget::FunctionDef(function) => function.inferred_type(model),
134+
GotoTarget::ClassDef(class) => class.inferred_type(model),
135+
GotoTarget::Parameter(parameter) => parameter.inferred_type(model),
136+
GotoTarget::Alias(alias) => alias.inferred_type(model),
137+
GotoTarget::ExceptVariable(except) => except.inferred_type(model),
138+
GotoTarget::KeywordArgument(argument) => {
139+
// TODO: Pyright resolves the declared type of the matching parameter. This seems more accurate
140+
// than using the inferred value.
141+
argument.value.inferred_type(model)
142+
}
143+
// TODO: Support identifier targets
144+
GotoTarget::PatternMatchRest(_)
145+
| GotoTarget::PatternKeywordArgument(_)
146+
| GotoTarget::PatternMatchStarName(_)
147+
| GotoTarget::PatternMatchAsName(_)
148+
| GotoTarget::ImportedModule(_)
149+
| GotoTarget::TypeParamTypeVarName(_)
150+
| GotoTarget::TypeParamParamSpecName(_)
151+
| GotoTarget::TypeParamTypeVarTupleName(_)
152+
| GotoTarget::NonLocal { .. }
153+
| GotoTarget::Globals { .. } => return None,
154+
};
155+
156+
Some(ty)
157+
}
158+
}
159+
152160
impl Ranged for GotoTarget<'_> {
153161
fn range(&self) -> TextRange {
154162
match self {
@@ -174,16 +182,18 @@ impl Ranged for GotoTarget<'_> {
174182
}
175183

176184
pub(crate) fn find_goto_target(parsed: &ParsedModule, offset: TextSize) -> Option<GotoTarget> {
177-
let token = parsed.tokens().at_offset(offset).find(|token| {
178-
matches!(
179-
token.kind(),
185+
let token = parsed
186+
.tokens()
187+
.at_offset(offset)
188+
.max_by_key(|token| match token.kind() {
180189
TokenKind::Name
181-
| TokenKind::String
182-
| TokenKind::Complex
183-
| TokenKind::Float
184-
| TokenKind::Int
185-
)
186-
})?;
190+
| TokenKind::String
191+
| TokenKind::Complex
192+
| TokenKind::Float
193+
| TokenKind::Int => 1,
194+
_ => 0,
195+
})?;
196+
187197
let covering_node = covering_node(parsed.syntax().into(), token.range())
188198
.find(|node| node.is_identifier() || node.is_expression())
189199
.ok()?;
@@ -241,27 +251,18 @@ pub(crate) fn find_goto_target(parsed: &ParsedModule, offset: TextSize) -> Optio
241251

242252
#[cfg(test)]
243253
mod tests {
244-
use std::fmt::Write;
245-
246-
use crate::db::tests::TestDb;
254+
use crate::tests::{cursor_test, CursorTest, IntoDiagnostic};
247255
use crate::{goto_type_definition, NavigationTarget};
248256
use insta::assert_snapshot;
249-
use insta::internals::SettingsBindDropGuard;
250-
use red_knot_python_semantic::{
251-
Program, ProgramSettings, PythonPath, PythonPlatform, SearchPathSettings,
252-
};
253257
use ruff_db::diagnostic::{
254-
Annotation, Diagnostic, DiagnosticFormat, DiagnosticId, DisplayDiagnosticConfig, LintName,
255-
Severity, Span, SubDiagnostic,
258+
Annotation, Diagnostic, DiagnosticId, LintName, Severity, Span, SubDiagnostic,
256259
};
257-
use ruff_db::files::{system_path_to_file, File, FileRange};
258-
use ruff_db::system::{DbWithWritableSystem, SystemPath, SystemPathBuf};
259-
use ruff_python_ast::PythonVersion;
260-
use ruff_text_size::{Ranged, TextSize};
260+
use ruff_db::files::FileRange;
261+
use ruff_text_size::Ranged;
261262

262263
#[test]
263264
fn goto_type_of_expression_with_class_type() {
264-
let test = goto_test(
265+
let test = cursor_test(
265266
r#"
266267
class Test: ...
267268
@@ -291,7 +292,7 @@ mod tests {
291292

292293
#[test]
293294
fn goto_type_of_expression_with_function_type() {
294-
let test = goto_test(
295+
let test = cursor_test(
295296
r#"
296297
def foo(a, b): ...
297298
@@ -323,7 +324,7 @@ mod tests {
323324

324325
#[test]
325326
fn goto_type_of_expression_with_union_type() {
326-
let test = goto_test(
327+
let test = cursor_test(
327328
r#"
328329
329330
def foo(a, b): ...
@@ -380,7 +381,7 @@ mod tests {
380381

381382
#[test]
382383
fn goto_type_of_expression_with_module() {
383-
let mut test = goto_test(
384+
let mut test = cursor_test(
384385
r#"
385386
import lib
386387
@@ -410,7 +411,7 @@ mod tests {
410411

411412
#[test]
412413
fn goto_type_of_expression_with_literal_type() {
413-
let test = goto_test(
414+
let test = cursor_test(
414415
r#"
415416
a: str = "test"
416417
@@ -441,7 +442,7 @@ mod tests {
441442
}
442443
#[test]
443444
fn goto_type_of_expression_with_literal_node() {
444-
let test = goto_test(
445+
let test = cursor_test(
445446
r#"
446447
a: str = "te<CURSOR>st"
447448
"#,
@@ -469,7 +470,7 @@ mod tests {
469470

470471
#[test]
471472
fn goto_type_of_expression_with_type_var_type() {
472-
let test = goto_test(
473+
let test = cursor_test(
473474
r#"
474475
type Alias[T: int = bool] = list[T<CURSOR>]
475476
"#,
@@ -493,7 +494,7 @@ mod tests {
493494

494495
#[test]
495496
fn goto_type_of_expression_with_type_param_spec() {
496-
let test = goto_test(
497+
let test = cursor_test(
497498
r#"
498499
type Alias[**P = [int, str]] = Callable[P<CURSOR>, int]
499500
"#,
@@ -507,7 +508,7 @@ mod tests {
507508

508509
#[test]
509510
fn goto_type_of_expression_with_type_var_tuple() {
510-
let test = goto_test(
511+
let test = cursor_test(
511512
r#"
512513
type Alias[*Ts = ()] = tuple[*Ts<CURSOR>]
513514
"#,
@@ -521,7 +522,7 @@ mod tests {
521522

522523
#[test]
523524
fn goto_type_on_keyword_argument() {
524-
let test = goto_test(
525+
let test = cursor_test(
525526
r#"
526527
def test(a: str): ...
527528
@@ -553,7 +554,7 @@ mod tests {
553554

554555
#[test]
555556
fn goto_type_on_incorrectly_typed_keyword_argument() {
556-
let test = goto_test(
557+
let test = cursor_test(
557558
r#"
558559
def test(a: str): ...
559560
@@ -588,7 +589,7 @@ mod tests {
588589

589590
#[test]
590591
fn goto_type_on_kwargs() {
591-
let test = goto_test(
592+
let test = cursor_test(
592593
r#"
593594
def f(name: str): ...
594595
@@ -622,14 +623,13 @@ f(**kwargs<CURSOR>)
622623

623624
#[test]
624625
fn goto_type_of_expression_with_builtin() {
625-
let test = goto_test(
626+
let test = cursor_test(
626627
r#"
627628
def foo(a: str):
628629
a<CURSOR>
629630
"#,
630631
);
631632

632-
// FIXME: This should go to `str`
633633
assert_snapshot!(test.goto_type_definition(), @r###"
634634
info: lint:goto-type-definition: Type definition
635635
--> stdlib/builtins.pyi:443:7
@@ -653,7 +653,7 @@ f(**kwargs<CURSOR>)
653653

654654
#[test]
655655
fn goto_type_definition_cursor_between_object_and_attribute() {
656-
let test = goto_test(
656+
let test = cursor_test(
657657
r#"
658658
class X:
659659
def foo(a, b): ...
@@ -685,7 +685,7 @@ f(**kwargs<CURSOR>)
685685

686686
#[test]
687687
fn goto_between_call_arguments() {
688-
let test = goto_test(
688+
let test = cursor_test(
689689
r#"
690690
def foo(a, b): ...
691691
@@ -715,7 +715,7 @@ f(**kwargs<CURSOR>)
715715

716716
#[test]
717717
fn goto_type_narrowing() {
718-
let test = goto_test(
718+
let test = cursor_test(
719719
r#"
720720
def foo(a: str | None, b):
721721
if a is not None:
@@ -747,7 +747,7 @@ f(**kwargs<CURSOR>)
747747

748748
#[test]
749749
fn goto_type_none() {
750-
let test = goto_test(
750+
let test = cursor_test(
751751
r#"
752752
def foo(a: str | None, b):
753753
a<CURSOR>
@@ -792,65 +792,7 @@ f(**kwargs<CURSOR>)
792792
");
793793
}
794794

795-
fn goto_test(source: &str) -> GotoTest {
796-
let mut db = TestDb::new();
797-
let cursor_offset = source.find("<CURSOR>").expect(
798-
"`source`` should contain a `<CURSOR>` marker, indicating the position of the cursor.",
799-
);
800-
801-
let mut content = source[..cursor_offset].to_string();
802-
content.push_str(&source[cursor_offset + "<CURSOR>".len()..]);
803-
804-
db.write_file("main.py", &content)
805-
.expect("write to memory file system to be successful");
806-
807-
let file = system_path_to_file(&db, "main.py").expect("newly written file to existing");
808-
809-
Program::from_settings(
810-
&db,
811-
ProgramSettings {
812-
python_version: PythonVersion::latest(),
813-
python_platform: PythonPlatform::default(),
814-
search_paths: SearchPathSettings {
815-
extra_paths: vec![],
816-
src_roots: vec![SystemPathBuf::from("/")],
817-
custom_typeshed: None,
818-
python_path: PythonPath::KnownSitePackages(vec![]),
819-
},
820-
},
821-
)
822-
.expect("Default settings to be valid");
823-
824-
let mut insta_settings = insta::Settings::clone_current();
825-
insta_settings.add_filter(r#"\\(\w\w|\s|\.|")"#, "/$1");
826-
827-
let insta_settings_guard = insta_settings.bind_to_scope();
828-
829-
GotoTest {
830-
db,
831-
cursor_offset: TextSize::try_from(cursor_offset)
832-
.expect("source to be smaller than 4GB"),
833-
file,
834-
_insta_settings_guard: insta_settings_guard,
835-
}
836-
}
837-
838-
struct GotoTest {
839-
db: TestDb,
840-
cursor_offset: TextSize,
841-
file: File,
842-
_insta_settings_guard: SettingsBindDropGuard,
843-
}
844-
845-
impl GotoTest {
846-
fn write_file(
847-
&mut self,
848-
path: impl AsRef<SystemPath>,
849-
content: &str,
850-
) -> std::io::Result<()> {
851-
self.db.write_file(path, content)
852-
}
853-
795+
impl CursorTest {
854796
fn goto_type_definition(&self) -> String {
855797
let Some(targets) = goto_type_definition(&self.db, self.file, self.cursor_offset)
856798
else {
@@ -861,19 +803,12 @@ f(**kwargs<CURSOR>)
861803
return "No type definitions found".to_string();
862804
}
863805

864-
let mut buf = String::new();
865-
866806
let source = targets.range;
867-
868-
let config = DisplayDiagnosticConfig::default()
869-
.color(false)
870-
.format(DiagnosticFormat::Full);
871-
for target in &*targets {
872-
let diag = GotoTypeDefinitionDiagnostic::new(source, target).into_diagnostic();
873-
write!(buf, "{}", diag.display(&self.db, &config)).unwrap();
874-
}
875-
876-
buf
807+
self.render_diagnostics(
808+
targets
809+
.into_iter()
810+
.map(|target| GotoTypeDefinitionDiagnostic::new(source, &target)),
811+
)
877812
}
878813
}
879814

@@ -889,7 +824,9 @@ f(**kwargs<CURSOR>)
889824
target: FileRange::new(target.file(), target.focus_range()),
890825
}
891826
}
827+
}
892828

829+
impl IntoDiagnostic for GotoTypeDefinitionDiagnostic {
893830
fn into_diagnostic(self) -> Diagnostic {
894831
let mut source = SubDiagnostic::new(Severity::Info, "Source");
895832
source.annotate(Annotation::primary(

0 commit comments

Comments
 (0)