Skip to content

Commit ca67635

Browse files
committed
[red-knot] LSP and playground hover
1 parent debd0c8 commit ca67635

8 files changed

Lines changed: 231 additions & 5 deletions

File tree

crates/red_knot_ide/src/hover.rs

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
use crate::goto::{find_goto_target, GotoTarget};
2+
use crate::{Db, RangedValue};
3+
use red_knot_python_semantic::types::Type;
4+
use red_knot_python_semantic::{HasType, SemanticModel};
5+
use ruff_db::files::{File, FileRange};
6+
use ruff_db::parsed::parsed_module;
7+
use ruff_text_size::{Ranged, TextSize};
8+
9+
pub fn hover(db: &dyn Db, file: File, offset: TextSize) -> Option<RangedValue<Hover>> {
10+
let parsed = parsed_module(db.upcast(), file);
11+
let goto_target = find_goto_target(parsed, offset)?;
12+
13+
let model = SemanticModel::new(db.upcast(), file);
14+
15+
let ty = match goto_target {
16+
GotoTarget::Expression(expression) => expression.inferred_type(&model),
17+
GotoTarget::FunctionDef(function) => function.inferred_type(&model),
18+
GotoTarget::ClassDef(class) => class.inferred_type(&model),
19+
GotoTarget::Parameter(parameter) => parameter.inferred_type(&model),
20+
GotoTarget::Alias(alias) => alias.inferred_type(&model),
21+
GotoTarget::ExceptVariable(except) => except.inferred_type(&model),
22+
GotoTarget::KeywordArgument(argument) => {
23+
// TODO: Pyright resolves the declared type of the matching parameter. This seems more accurate
24+
// than using the inferred value.
25+
argument.value.inferred_type(&model)
26+
}
27+
28+
// TODO: Better support for go to type definition in match pattern.
29+
// This may require improving type inference (e.g. it currently doesn't handle `...rest`)
30+
// but it also requires a new API to query the type because implementing `HasType` for `PatternMatchMapping`
31+
// is ambiguous.
32+
GotoTarget::PatternMatchRest(_)
33+
| GotoTarget::PatternKeywordArgument(_)
34+
| GotoTarget::PatternMatchStarName(_)
35+
| GotoTarget::PatternMatchAsName(_) => return None,
36+
37+
// TODO: Resolve the module; The type inference already does all the work
38+
// but type isn't stored anywhere. We should either extract the logic
39+
// for resolving the module from a ImportFromStmt or store the type during semantic analysis
40+
GotoTarget::ImportedModule(_) => return None,
41+
42+
// Targets without a type definition.
43+
GotoTarget::TypeParamTypeVarName(_)
44+
| GotoTarget::TypeParamParamSpecName(_)
45+
| GotoTarget::TypeParamTypeVarTupleName(_) => return None,
46+
GotoTarget::NonLocal { .. } | GotoTarget::Globals { .. } => return None,
47+
};
48+
49+
// TODO: Most LSPs show the symbol declaration: e.g. `class Foo: x: str` instead of just the name of the type
50+
// It also seems possible to return more than one markdown element and clients then display all of the
51+
// markdown blocks.
52+
tracing::debug!(
53+
"Inferred type of covering node is {}",
54+
ty.display(db.upcast())
55+
);
56+
57+
Some(RangedValue {
58+
range: FileRange::new(file, goto_target.range()),
59+
value: Hover::Type(ty),
60+
})
61+
}
62+
63+
pub enum Hover<'db> {
64+
Type(Type<'db>),
65+
}
66+
67+
impl Hover<'_> {
68+
pub fn to_string(&self, db: &dyn Db) -> String {
69+
match self {
70+
Hover::Type(ty) => ty.display(db.upcast()).to_string(),
71+
}
72+
}
73+
}

crates/red_knot_ide/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
mod db;
22
mod find_node;
33
mod goto;
4+
mod hover;
45

56
use std::ops::{Deref, DerefMut};
67

78
pub use db::Db;
89
pub use goto::go_to_type_definition;
10+
pub use hover::hover;
911
use red_knot_python_semantic::types::{
1012
ClassLiteralType, FunctionType, InstanceType, KnownInstanceType, ModuleLiteralType, Type,
1113
};

crates/red_knot_server/src/server.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@ use std::panic::PanicInfo;
77

88
use lsp_server::Message;
99
use lsp_types::{
10-
ClientCapabilities, DiagnosticOptions, DiagnosticServerCapabilities, MessageType,
11-
ServerCapabilities, TextDocumentSyncCapability, TextDocumentSyncKind, TextDocumentSyncOptions,
12-
TypeDefinitionProviderCapability, Url,
10+
ClientCapabilities, DiagnosticOptions, DiagnosticServerCapabilities, HoverProviderCapability,
11+
MessageType, ServerCapabilities, TextDocumentSyncCapability, TextDocumentSyncKind,
12+
TextDocumentSyncOptions, TypeDefinitionProviderCapability, Url,
1313
};
1414

1515
use self::connection::{Connection, ConnectionInitializer};
@@ -221,6 +221,7 @@ impl Server {
221221
},
222222
)),
223223
type_definition_provider: Some(TypeDefinitionProviderCapability::Simple(true)),
224+
hover_provider: Some(HoverProviderCapability::Simple(true)),
224225
..Default::default()
225226
}
226227
}

crates/red_knot_server/src/server/api.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ pub(super) fn request<'a>(req: server::Request) -> Task<'a> {
3232
BackgroundSchedule::LatencySensitive,
3333
)
3434
}
35+
request::HoverRequestHandler::METHOD => background_request_task::<
36+
request::HoverRequestHandler,
37+
>(req, BackgroundSchedule::LatencySensitive),
38+
3539
method => {
3640
tracing::warn!("Received request {method} which does not have a handler");
3741
return Task::nothing();
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
mod diagnostic;
22
mod goto_type_definition;
3+
mod hover;
34

45
pub(super) use diagnostic::DocumentDiagnosticRequestHandler;
56
pub(super) use goto_type_definition::GotoTypeDefinitionRequestHandler;
7+
pub(super) use hover::HoverRequestHandler;
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
use std::borrow::Cow;
2+
3+
use crate::document::{PositionExt, ToRangeExt};
4+
use crate::server::api::traits::{BackgroundDocumentRequestHandler, RequestHandler};
5+
use crate::server::client::Notifier;
6+
use crate::DocumentSnapshot;
7+
use lsp_types::request::HoverRequest;
8+
use lsp_types::{HoverContents, HoverParams, MarkupContent, MarkupKind, Url};
9+
use red_knot_ide::hover;
10+
use red_knot_project::ProjectDatabase;
11+
use ruff_db::source::{line_index, source_text};
12+
use ruff_text_size::Ranged;
13+
14+
pub(crate) struct HoverRequestHandler;
15+
16+
impl RequestHandler for HoverRequestHandler {
17+
type RequestType = HoverRequest;
18+
}
19+
20+
impl BackgroundDocumentRequestHandler for HoverRequestHandler {
21+
fn document_url(params: &HoverParams) -> Cow<Url> {
22+
Cow::Borrowed(&params.text_document_position_params.text_document.uri)
23+
}
24+
25+
fn run_with_snapshot(
26+
snapshot: DocumentSnapshot,
27+
db: ProjectDatabase,
28+
_notifier: Notifier,
29+
params: HoverParams,
30+
) -> crate::server::Result<Option<lsp_types::Hover>> {
31+
let Some(file) = snapshot.file(&db) else {
32+
tracing::debug!("Failed to resolve file for {:?}", params);
33+
return Ok(None);
34+
};
35+
36+
let source = source_text(&db, file);
37+
let line_index = line_index(&db, file);
38+
let offset = params.text_document_position_params.position.to_text_size(
39+
&source,
40+
&line_index,
41+
snapshot.encoding(),
42+
);
43+
44+
let Some(range_info) = hover(&db, file, offset) else {
45+
return Ok(None);
46+
};
47+
48+
// TODO: Respect the clients preferred content format
49+
Ok(Some(lsp_types::Hover {
50+
contents: HoverContents::Markup(MarkupContent {
51+
kind: MarkupKind::Markdown,
52+
value: format!("```\n{ty}\n```", ty = range_info.to_string(&db)),
53+
}),
54+
range: Some(range_info.file_range().range().to_range(
55+
&source,
56+
&line_index,
57+
snapshot.encoding(),
58+
)),
59+
}))
60+
}
61+
}

crates/red_knot_wasm/src/lib.rs

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use std::any::Any;
22

33
use js_sys::{Error, JsString};
4-
use red_knot_ide::go_to_type_definition;
4+
use red_knot_ide::{go_to_type_definition, hover};
55
use red_knot_project::metadata::options::Options;
66
use red_knot_project::metadata::value::ValueSource;
77
use red_knot_project::watch::{ChangeEvent, ChangedKind, CreatedKind, DeletedKind};
@@ -243,6 +243,35 @@ impl Workspace {
243243

244244
Ok(links)
245245
}
246+
247+
#[wasm_bindgen]
248+
pub fn hover(&self, file_id: &FileHandle, position: Position) -> Result<Option<Hover>, Error> {
249+
let source = source_text(&self.db, file_id.file);
250+
let index = line_index(&self.db, file_id.file);
251+
252+
let offset = index.offset(
253+
OneIndexed::new(position.line).ok_or_else(|| {
254+
Error::new("Invalid value `0` for `position.line`. The line index is 1-indexed.")
255+
})?,
256+
OneIndexed::new(position.column).ok_or_else(|| {
257+
Error::new(
258+
"Invalid value `0` for `position.column`. The column index is 1-indexed.",
259+
)
260+
})?,
261+
&source,
262+
);
263+
264+
let Some(range_info) = hover(&self.db, file_id.file, offset) else {
265+
return Ok(None);
266+
};
267+
268+
let source_range = Range::from_text_range(range_info.file_range().range(), &index, &source);
269+
270+
Ok(Some(Hover {
271+
markdown: format!("```python\n{}\n```", range_info.to_string(&self.db)),
272+
range: source_range,
273+
}))
274+
}
246275
}
247276

248277
pub(crate) fn into_error<E: std::fmt::Display>(err: E) -> Error {
@@ -437,6 +466,14 @@ pub struct LocationLink {
437466
pub origin_selection_range: Option<Range>,
438467
}
439468

469+
#[wasm_bindgen]
470+
pub struct Hover {
471+
#[wasm_bindgen(getter_with_clone)]
472+
pub markdown: String,
473+
474+
pub range: Range,
475+
}
476+
440477
#[derive(Debug, Clone)]
441478
struct WasmSystem {
442479
fs: MemoryFileSystem,

playground/knot/src/Editor/Editor.tsx

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ export default function Editor({
5656
const disposable = useRef<{
5757
typeDefinition: IDisposable;
5858
editorOpener: IDisposable;
59+
hover: IDisposable;
5960
} | null>(null);
6061
const playgroundState = useRef<PlaygroundServerProps>({
6162
monaco: null,
@@ -93,6 +94,7 @@ export default function Editor({
9394
return () => {
9495
disposable.current?.typeDefinition.dispose();
9596
disposable.current?.editorOpener.dispose();
97+
disposable.current?.hover.dispose();
9698
};
9799
}, []);
98100

@@ -103,12 +105,17 @@ export default function Editor({
103105
const server = new PlaygroundServer(playgroundState);
104106
const typeDefinitionDisposable =
105107
instance.languages.registerTypeDefinitionProvider("python", server);
108+
const hoverDisposable = instance.languages.registerHoverProvider(
109+
"python",
110+
server,
111+
);
106112
const editorOpenerDisposable =
107113
instance.editor.registerEditorOpener(server);
108114

109115
disposable.current = {
110116
typeDefinition: typeDefinitionDisposable,
111117
editorOpener: editorOpenerDisposable,
118+
hover: hoverDisposable,
112119
};
113120

114121
playgroundState.current.monaco = instance;
@@ -195,10 +202,49 @@ interface PlaygroundServerProps {
195202
}
196203

197204
class PlaygroundServer
198-
implements languages.TypeDefinitionProvider, editor.ICodeEditorOpener
205+
implements
206+
languages.TypeDefinitionProvider,
207+
editor.ICodeEditorOpener,
208+
languages.HoverProvider
199209
{
200210
constructor(private props: RefObject<PlaygroundServerProps>) {}
201211

212+
provideHover(
213+
model: editor.ITextModel,
214+
position: Position,
215+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
216+
_token: CancellationToken,
217+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
218+
context?: languages.HoverContext<languages.Hover> | undefined,
219+
): languages.ProviderResult<languages.Hover> {
220+
const workspace = this.props.current.workspace;
221+
222+
const selectedFile = this.props.current.files.selected;
223+
if (selectedFile == null) {
224+
return;
225+
}
226+
227+
const selectedHandle = this.props.current.files.handles[selectedFile];
228+
229+
if (selectedHandle == null) {
230+
return;
231+
}
232+
233+
const hover = workspace.hover(
234+
selectedHandle,
235+
new KnotPosition(position.lineNumber, position.column),
236+
);
237+
238+
if (hover == null) {
239+
return;
240+
}
241+
242+
return {
243+
range: knotRangeToIRange(hover.range),
244+
contents: [{ value: hover.markdown, isTrusted: true }],
245+
};
246+
}
247+
202248
provideTypeDefinition(
203249
model: editor.ITextModel,
204250
position: Position,

0 commit comments

Comments
 (0)