mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-27 12:29:48 +00:00
[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
54f7da25f9/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](https://github.com/astral-sh/ruff/issues/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
This commit is contained in:
parent
bf0306887a
commit
a4ba10ff0a
15 changed files with 998 additions and 157 deletions
|
@ -1,5 +1,6 @@
|
||||||
use crate::find_node::covering_node;
|
use crate::find_node::covering_node;
|
||||||
use crate::{Db, HasNavigationTargets, NavigationTargets, RangedValue};
|
use crate::{Db, HasNavigationTargets, NavigationTargets, RangedValue};
|
||||||
|
use red_knot_python_semantic::types::Type;
|
||||||
use red_knot_python_semantic::{HasType, SemanticModel};
|
use red_knot_python_semantic::{HasType, SemanticModel};
|
||||||
use ruff_db::files::{File, FileRange};
|
use ruff_db::files::{File, FileRange};
|
||||||
use ruff_db::parsed::{parsed_module, ParsedModule};
|
use ruff_db::parsed::{parsed_module, ParsedModule};
|
||||||
|
@ -16,31 +17,7 @@ pub fn goto_type_definition(
|
||||||
let goto_target = find_goto_target(parsed, offset)?;
|
let goto_target = find_goto_target(parsed, offset)?;
|
||||||
|
|
||||||
let model = SemanticModel::new(db.upcast(), file);
|
let model = SemanticModel::new(db.upcast(), file);
|
||||||
|
let ty = goto_target.inferred_type(&model)?;
|
||||||
let ty = match goto_target {
|
|
||||||
GotoTarget::Expression(expression) => expression.inferred_type(&model),
|
|
||||||
GotoTarget::FunctionDef(function) => function.inferred_type(&model),
|
|
||||||
GotoTarget::ClassDef(class) => class.inferred_type(&model),
|
|
||||||
GotoTarget::Parameter(parameter) => parameter.inferred_type(&model),
|
|
||||||
GotoTarget::Alias(alias) => alias.inferred_type(&model),
|
|
||||||
GotoTarget::ExceptVariable(except) => except.inferred_type(&model),
|
|
||||||
GotoTarget::KeywordArgument(argument) => {
|
|
||||||
// TODO: Pyright resolves the declared type of the matching parameter. This seems more accurate
|
|
||||||
// than using the inferred value.
|
|
||||||
argument.value.inferred_type(&model)
|
|
||||||
}
|
|
||||||
// TODO: Support identifier targets
|
|
||||||
GotoTarget::PatternMatchRest(_)
|
|
||||||
| GotoTarget::PatternKeywordArgument(_)
|
|
||||||
| GotoTarget::PatternMatchStarName(_)
|
|
||||||
| GotoTarget::PatternMatchAsName(_)
|
|
||||||
| GotoTarget::ImportedModule(_)
|
|
||||||
| GotoTarget::TypeParamTypeVarName(_)
|
|
||||||
| GotoTarget::TypeParamParamSpecName(_)
|
|
||||||
| GotoTarget::TypeParamTypeVarTupleName(_)
|
|
||||||
| GotoTarget::NonLocal { .. }
|
|
||||||
| GotoTarget::Globals { .. } => return None,
|
|
||||||
};
|
|
||||||
|
|
||||||
tracing::debug!(
|
tracing::debug!(
|
||||||
"Inferred type of covering node is {}",
|
"Inferred type of covering node is {}",
|
||||||
|
@ -149,6 +126,37 @@ pub(crate) enum GotoTarget<'a> {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<'db> GotoTarget<'db> {
|
||||||
|
pub(crate) fn inferred_type(self, model: &SemanticModel<'db>) -> Option<Type<'db>> {
|
||||||
|
let ty = match self {
|
||||||
|
GotoTarget::Expression(expression) => expression.inferred_type(model),
|
||||||
|
GotoTarget::FunctionDef(function) => function.inferred_type(model),
|
||||||
|
GotoTarget::ClassDef(class) => class.inferred_type(model),
|
||||||
|
GotoTarget::Parameter(parameter) => parameter.inferred_type(model),
|
||||||
|
GotoTarget::Alias(alias) => alias.inferred_type(model),
|
||||||
|
GotoTarget::ExceptVariable(except) => except.inferred_type(model),
|
||||||
|
GotoTarget::KeywordArgument(argument) => {
|
||||||
|
// TODO: Pyright resolves the declared type of the matching parameter. This seems more accurate
|
||||||
|
// than using the inferred value.
|
||||||
|
argument.value.inferred_type(model)
|
||||||
|
}
|
||||||
|
// TODO: Support identifier targets
|
||||||
|
GotoTarget::PatternMatchRest(_)
|
||||||
|
| GotoTarget::PatternKeywordArgument(_)
|
||||||
|
| GotoTarget::PatternMatchStarName(_)
|
||||||
|
| GotoTarget::PatternMatchAsName(_)
|
||||||
|
| GotoTarget::ImportedModule(_)
|
||||||
|
| GotoTarget::TypeParamTypeVarName(_)
|
||||||
|
| GotoTarget::TypeParamParamSpecName(_)
|
||||||
|
| GotoTarget::TypeParamTypeVarTupleName(_)
|
||||||
|
| GotoTarget::NonLocal { .. }
|
||||||
|
| GotoTarget::Globals { .. } => return None,
|
||||||
|
};
|
||||||
|
|
||||||
|
Some(ty)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Ranged for GotoTarget<'_> {
|
impl Ranged for GotoTarget<'_> {
|
||||||
fn range(&self) -> TextRange {
|
fn range(&self) -> TextRange {
|
||||||
match self {
|
match self {
|
||||||
|
@ -174,16 +182,18 @@ impl Ranged for GotoTarget<'_> {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn find_goto_target(parsed: &ParsedModule, offset: TextSize) -> Option<GotoTarget> {
|
pub(crate) fn find_goto_target(parsed: &ParsedModule, offset: TextSize) -> Option<GotoTarget> {
|
||||||
let token = parsed.tokens().at_offset(offset).find(|token| {
|
let token = parsed
|
||||||
matches!(
|
.tokens()
|
||||||
token.kind(),
|
.at_offset(offset)
|
||||||
|
.max_by_key(|token| match token.kind() {
|
||||||
TokenKind::Name
|
TokenKind::Name
|
||||||
| TokenKind::String
|
| TokenKind::String
|
||||||
| TokenKind::Complex
|
| TokenKind::Complex
|
||||||
| TokenKind::Float
|
| TokenKind::Float
|
||||||
| TokenKind::Int
|
| TokenKind::Int => 1,
|
||||||
)
|
_ => 0,
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let covering_node = covering_node(parsed.syntax().into(), token.range())
|
let covering_node = covering_node(parsed.syntax().into(), token.range())
|
||||||
.find(|node| node.is_identifier() || node.is_expression())
|
.find(|node| node.is_identifier() || node.is_expression())
|
||||||
.ok()?;
|
.ok()?;
|
||||||
|
@ -241,27 +251,18 @@ pub(crate) fn find_goto_target(parsed: &ParsedModule, offset: TextSize) -> Optio
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use std::fmt::Write;
|
use crate::tests::{cursor_test, CursorTest, IntoDiagnostic};
|
||||||
|
|
||||||
use crate::db::tests::TestDb;
|
|
||||||
use crate::{goto_type_definition, NavigationTarget};
|
use crate::{goto_type_definition, NavigationTarget};
|
||||||
use insta::assert_snapshot;
|
use insta::assert_snapshot;
|
||||||
use insta::internals::SettingsBindDropGuard;
|
|
||||||
use red_knot_python_semantic::{
|
|
||||||
Program, ProgramSettings, PythonPath, PythonPlatform, SearchPathSettings,
|
|
||||||
};
|
|
||||||
use ruff_db::diagnostic::{
|
use ruff_db::diagnostic::{
|
||||||
Annotation, Diagnostic, DiagnosticFormat, DiagnosticId, DisplayDiagnosticConfig, LintName,
|
Annotation, Diagnostic, DiagnosticId, LintName, Severity, Span, SubDiagnostic,
|
||||||
Severity, Span, SubDiagnostic,
|
|
||||||
};
|
};
|
||||||
use ruff_db::files::{system_path_to_file, File, FileRange};
|
use ruff_db::files::FileRange;
|
||||||
use ruff_db::system::{DbWithWritableSystem, SystemPath, SystemPathBuf};
|
use ruff_text_size::Ranged;
|
||||||
use ruff_python_ast::PythonVersion;
|
|
||||||
use ruff_text_size::{Ranged, TextSize};
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn goto_type_of_expression_with_class_type() {
|
fn goto_type_of_expression_with_class_type() {
|
||||||
let test = goto_test(
|
let test = cursor_test(
|
||||||
r#"
|
r#"
|
||||||
class Test: ...
|
class Test: ...
|
||||||
|
|
||||||
|
@ -291,7 +292,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn goto_type_of_expression_with_function_type() {
|
fn goto_type_of_expression_with_function_type() {
|
||||||
let test = goto_test(
|
let test = cursor_test(
|
||||||
r#"
|
r#"
|
||||||
def foo(a, b): ...
|
def foo(a, b): ...
|
||||||
|
|
||||||
|
@ -323,7 +324,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn goto_type_of_expression_with_union_type() {
|
fn goto_type_of_expression_with_union_type() {
|
||||||
let test = goto_test(
|
let test = cursor_test(
|
||||||
r#"
|
r#"
|
||||||
|
|
||||||
def foo(a, b): ...
|
def foo(a, b): ...
|
||||||
|
@ -380,7 +381,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn goto_type_of_expression_with_module() {
|
fn goto_type_of_expression_with_module() {
|
||||||
let mut test = goto_test(
|
let mut test = cursor_test(
|
||||||
r#"
|
r#"
|
||||||
import lib
|
import lib
|
||||||
|
|
||||||
|
@ -410,7 +411,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn goto_type_of_expression_with_literal_type() {
|
fn goto_type_of_expression_with_literal_type() {
|
||||||
let test = goto_test(
|
let test = cursor_test(
|
||||||
r#"
|
r#"
|
||||||
a: str = "test"
|
a: str = "test"
|
||||||
|
|
||||||
|
@ -441,7 +442,7 @@ mod tests {
|
||||||
}
|
}
|
||||||
#[test]
|
#[test]
|
||||||
fn goto_type_of_expression_with_literal_node() {
|
fn goto_type_of_expression_with_literal_node() {
|
||||||
let test = goto_test(
|
let test = cursor_test(
|
||||||
r#"
|
r#"
|
||||||
a: str = "te<CURSOR>st"
|
a: str = "te<CURSOR>st"
|
||||||
"#,
|
"#,
|
||||||
|
@ -469,7 +470,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn goto_type_of_expression_with_type_var_type() {
|
fn goto_type_of_expression_with_type_var_type() {
|
||||||
let test = goto_test(
|
let test = cursor_test(
|
||||||
r#"
|
r#"
|
||||||
type Alias[T: int = bool] = list[T<CURSOR>]
|
type Alias[T: int = bool] = list[T<CURSOR>]
|
||||||
"#,
|
"#,
|
||||||
|
@ -493,7 +494,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn goto_type_of_expression_with_type_param_spec() {
|
fn goto_type_of_expression_with_type_param_spec() {
|
||||||
let test = goto_test(
|
let test = cursor_test(
|
||||||
r#"
|
r#"
|
||||||
type Alias[**P = [int, str]] = Callable[P<CURSOR>, int]
|
type Alias[**P = [int, str]] = Callable[P<CURSOR>, int]
|
||||||
"#,
|
"#,
|
||||||
|
@ -507,7 +508,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn goto_type_of_expression_with_type_var_tuple() {
|
fn goto_type_of_expression_with_type_var_tuple() {
|
||||||
let test = goto_test(
|
let test = cursor_test(
|
||||||
r#"
|
r#"
|
||||||
type Alias[*Ts = ()] = tuple[*Ts<CURSOR>]
|
type Alias[*Ts = ()] = tuple[*Ts<CURSOR>]
|
||||||
"#,
|
"#,
|
||||||
|
@ -521,7 +522,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn goto_type_on_keyword_argument() {
|
fn goto_type_on_keyword_argument() {
|
||||||
let test = goto_test(
|
let test = cursor_test(
|
||||||
r#"
|
r#"
|
||||||
def test(a: str): ...
|
def test(a: str): ...
|
||||||
|
|
||||||
|
@ -553,7 +554,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn goto_type_on_incorrectly_typed_keyword_argument() {
|
fn goto_type_on_incorrectly_typed_keyword_argument() {
|
||||||
let test = goto_test(
|
let test = cursor_test(
|
||||||
r#"
|
r#"
|
||||||
def test(a: str): ...
|
def test(a: str): ...
|
||||||
|
|
||||||
|
@ -588,7 +589,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn goto_type_on_kwargs() {
|
fn goto_type_on_kwargs() {
|
||||||
let test = goto_test(
|
let test = cursor_test(
|
||||||
r#"
|
r#"
|
||||||
def f(name: str): ...
|
def f(name: str): ...
|
||||||
|
|
||||||
|
@ -622,14 +623,13 @@ f(**kwargs<CURSOR>)
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn goto_type_of_expression_with_builtin() {
|
fn goto_type_of_expression_with_builtin() {
|
||||||
let test = goto_test(
|
let test = cursor_test(
|
||||||
r#"
|
r#"
|
||||||
def foo(a: str):
|
def foo(a: str):
|
||||||
a<CURSOR>
|
a<CURSOR>
|
||||||
"#,
|
"#,
|
||||||
);
|
);
|
||||||
|
|
||||||
// FIXME: This should go to `str`
|
|
||||||
assert_snapshot!(test.goto_type_definition(), @r###"
|
assert_snapshot!(test.goto_type_definition(), @r###"
|
||||||
info: lint:goto-type-definition: Type definition
|
info: lint:goto-type-definition: Type definition
|
||||||
--> stdlib/builtins.pyi:443:7
|
--> stdlib/builtins.pyi:443:7
|
||||||
|
@ -653,7 +653,7 @@ f(**kwargs<CURSOR>)
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn goto_type_definition_cursor_between_object_and_attribute() {
|
fn goto_type_definition_cursor_between_object_and_attribute() {
|
||||||
let test = goto_test(
|
let test = cursor_test(
|
||||||
r#"
|
r#"
|
||||||
class X:
|
class X:
|
||||||
def foo(a, b): ...
|
def foo(a, b): ...
|
||||||
|
@ -685,7 +685,7 @@ f(**kwargs<CURSOR>)
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn goto_between_call_arguments() {
|
fn goto_between_call_arguments() {
|
||||||
let test = goto_test(
|
let test = cursor_test(
|
||||||
r#"
|
r#"
|
||||||
def foo(a, b): ...
|
def foo(a, b): ...
|
||||||
|
|
||||||
|
@ -715,7 +715,7 @@ f(**kwargs<CURSOR>)
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn goto_type_narrowing() {
|
fn goto_type_narrowing() {
|
||||||
let test = goto_test(
|
let test = cursor_test(
|
||||||
r#"
|
r#"
|
||||||
def foo(a: str | None, b):
|
def foo(a: str | None, b):
|
||||||
if a is not None:
|
if a is not None:
|
||||||
|
@ -747,7 +747,7 @@ f(**kwargs<CURSOR>)
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn goto_type_none() {
|
fn goto_type_none() {
|
||||||
let test = goto_test(
|
let test = cursor_test(
|
||||||
r#"
|
r#"
|
||||||
def foo(a: str | None, b):
|
def foo(a: str | None, b):
|
||||||
a<CURSOR>
|
a<CURSOR>
|
||||||
|
@ -792,65 +792,7 @@ f(**kwargs<CURSOR>)
|
||||||
");
|
");
|
||||||
}
|
}
|
||||||
|
|
||||||
fn goto_test(source: &str) -> GotoTest {
|
impl CursorTest {
|
||||||
let mut db = TestDb::new();
|
|
||||||
let cursor_offset = source.find("<CURSOR>").expect(
|
|
||||||
"`source`` should contain a `<CURSOR>` marker, indicating the position of the cursor.",
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut content = source[..cursor_offset].to_string();
|
|
||||||
content.push_str(&source[cursor_offset + "<CURSOR>".len()..]);
|
|
||||||
|
|
||||||
db.write_file("main.py", &content)
|
|
||||||
.expect("write to memory file system to be successful");
|
|
||||||
|
|
||||||
let file = system_path_to_file(&db, "main.py").expect("newly written file to existing");
|
|
||||||
|
|
||||||
Program::from_settings(
|
|
||||||
&db,
|
|
||||||
ProgramSettings {
|
|
||||||
python_version: PythonVersion::latest(),
|
|
||||||
python_platform: PythonPlatform::default(),
|
|
||||||
search_paths: SearchPathSettings {
|
|
||||||
extra_paths: vec![],
|
|
||||||
src_roots: vec![SystemPathBuf::from("/")],
|
|
||||||
custom_typeshed: None,
|
|
||||||
python_path: PythonPath::KnownSitePackages(vec![]),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.expect("Default settings to be valid");
|
|
||||||
|
|
||||||
let mut insta_settings = insta::Settings::clone_current();
|
|
||||||
insta_settings.add_filter(r#"\\(\w\w|\s|\.|")"#, "/$1");
|
|
||||||
|
|
||||||
let insta_settings_guard = insta_settings.bind_to_scope();
|
|
||||||
|
|
||||||
GotoTest {
|
|
||||||
db,
|
|
||||||
cursor_offset: TextSize::try_from(cursor_offset)
|
|
||||||
.expect("source to be smaller than 4GB"),
|
|
||||||
file,
|
|
||||||
_insta_settings_guard: insta_settings_guard,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct GotoTest {
|
|
||||||
db: TestDb,
|
|
||||||
cursor_offset: TextSize,
|
|
||||||
file: File,
|
|
||||||
_insta_settings_guard: SettingsBindDropGuard,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl GotoTest {
|
|
||||||
fn write_file(
|
|
||||||
&mut self,
|
|
||||||
path: impl AsRef<SystemPath>,
|
|
||||||
content: &str,
|
|
||||||
) -> std::io::Result<()> {
|
|
||||||
self.db.write_file(path, content)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn goto_type_definition(&self) -> String {
|
fn goto_type_definition(&self) -> String {
|
||||||
let Some(targets) = goto_type_definition(&self.db, self.file, self.cursor_offset)
|
let Some(targets) = goto_type_definition(&self.db, self.file, self.cursor_offset)
|
||||||
else {
|
else {
|
||||||
|
@ -861,19 +803,12 @@ f(**kwargs<CURSOR>)
|
||||||
return "No type definitions found".to_string();
|
return "No type definitions found".to_string();
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut buf = String::new();
|
|
||||||
|
|
||||||
let source = targets.range;
|
let source = targets.range;
|
||||||
|
self.render_diagnostics(
|
||||||
let config = DisplayDiagnosticConfig::default()
|
targets
|
||||||
.color(false)
|
.into_iter()
|
||||||
.format(DiagnosticFormat::Full);
|
.map(|target| GotoTypeDefinitionDiagnostic::new(source, &target)),
|
||||||
for target in &*targets {
|
)
|
||||||
let diag = GotoTypeDefinitionDiagnostic::new(source, target).into_diagnostic();
|
|
||||||
write!(buf, "{}", diag.display(&self.db, &config)).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
buf
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -889,7 +824,9 @@ f(**kwargs<CURSOR>)
|
||||||
target: FileRange::new(target.file(), target.focus_range()),
|
target: FileRange::new(target.file(), target.focus_range()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoDiagnostic for GotoTypeDefinitionDiagnostic {
|
||||||
fn into_diagnostic(self) -> Diagnostic {
|
fn into_diagnostic(self) -> Diagnostic {
|
||||||
let mut source = SubDiagnostic::new(Severity::Info, "Source");
|
let mut source = SubDiagnostic::new(Severity::Info, "Source");
|
||||||
source.annotate(Annotation::primary(
|
source.annotate(Annotation::primary(
|
||||||
|
|
545
crates/red_knot_ide/src/hover.rs
Normal file
545
crates/red_knot_ide/src/hover.rs
Normal file
|
@ -0,0 +1,545 @@
|
||||||
|
use crate::goto::find_goto_target;
|
||||||
|
use crate::{Db, MarkupKind, RangedValue};
|
||||||
|
use red_knot_python_semantic::types::Type;
|
||||||
|
use red_knot_python_semantic::SemanticModel;
|
||||||
|
use ruff_db::files::{File, FileRange};
|
||||||
|
use ruff_db::parsed::parsed_module;
|
||||||
|
use ruff_text_size::{Ranged, TextSize};
|
||||||
|
use std::fmt;
|
||||||
|
use std::fmt::Formatter;
|
||||||
|
|
||||||
|
pub fn hover(db: &dyn Db, file: File, offset: TextSize) -> Option<RangedValue<Hover>> {
|
||||||
|
let parsed = parsed_module(db.upcast(), file);
|
||||||
|
let goto_target = find_goto_target(parsed, offset)?;
|
||||||
|
|
||||||
|
let model = SemanticModel::new(db.upcast(), file);
|
||||||
|
let ty = goto_target.inferred_type(&model)?;
|
||||||
|
|
||||||
|
tracing::debug!(
|
||||||
|
"Inferred type of covering node is {}",
|
||||||
|
ty.display(db.upcast())
|
||||||
|
);
|
||||||
|
|
||||||
|
// TODO: Add documentation of the symbol (not the type's definition).
|
||||||
|
// TODO: Render the symbol's signature instead of just its type.
|
||||||
|
let contents = vec![HoverContent::Type(ty)];
|
||||||
|
|
||||||
|
Some(RangedValue {
|
||||||
|
range: FileRange::new(file, goto_target.range()),
|
||||||
|
value: Hover { contents },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Hover<'db> {
|
||||||
|
contents: Vec<HoverContent<'db>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'db> Hover<'db> {
|
||||||
|
/// Renders the hover to a string using the specified markup kind.
|
||||||
|
pub const fn display<'a>(&'a self, db: &'a dyn Db, kind: MarkupKind) -> DisplayHover<'a> {
|
||||||
|
DisplayHover {
|
||||||
|
db,
|
||||||
|
hover: self,
|
||||||
|
kind,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn iter(&self) -> std::slice::Iter<'_, HoverContent<'db>> {
|
||||||
|
self.contents.iter()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'db> IntoIterator for Hover<'db> {
|
||||||
|
type Item = HoverContent<'db>;
|
||||||
|
type IntoIter = std::vec::IntoIter<Self::Item>;
|
||||||
|
|
||||||
|
fn into_iter(self) -> Self::IntoIter {
|
||||||
|
self.contents.into_iter()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'db> IntoIterator for &'a Hover<'db> {
|
||||||
|
type Item = &'a HoverContent<'db>;
|
||||||
|
type IntoIter = std::slice::Iter<'a, HoverContent<'db>>;
|
||||||
|
|
||||||
|
fn into_iter(self) -> Self::IntoIter {
|
||||||
|
self.iter()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct DisplayHover<'a> {
|
||||||
|
db: &'a dyn Db,
|
||||||
|
hover: &'a Hover<'a>,
|
||||||
|
kind: MarkupKind,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for DisplayHover<'_> {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||||
|
let mut first = true;
|
||||||
|
for content in &self.hover.contents {
|
||||||
|
if !first {
|
||||||
|
self.kind.horizontal_line().fmt(f)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
content.display(self.db, self.kind).fmt(f)?;
|
||||||
|
first = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||||
|
pub enum HoverContent<'db> {
|
||||||
|
Type(Type<'db>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'db> HoverContent<'db> {
|
||||||
|
fn display(&self, db: &'db dyn Db, kind: MarkupKind) -> DisplayHoverContent<'_, 'db> {
|
||||||
|
DisplayHoverContent {
|
||||||
|
db,
|
||||||
|
content: self,
|
||||||
|
kind,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct DisplayHoverContent<'a, 'db> {
|
||||||
|
db: &'db dyn Db,
|
||||||
|
content: &'a HoverContent<'db>,
|
||||||
|
kind: MarkupKind,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for DisplayHoverContent<'_, '_> {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self.content {
|
||||||
|
HoverContent::Type(ty) => self
|
||||||
|
.kind
|
||||||
|
.fenced_code_block(ty.display(self.db.upcast()), "text")
|
||||||
|
.fmt(f),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::tests::{cursor_test, CursorTest};
|
||||||
|
use crate::{hover, MarkupKind};
|
||||||
|
use insta::assert_snapshot;
|
||||||
|
use ruff_db::diagnostic::{
|
||||||
|
Annotation, Diagnostic, DiagnosticFormat, DiagnosticId, DisplayDiagnosticConfig, LintName,
|
||||||
|
Severity, Span,
|
||||||
|
};
|
||||||
|
use ruff_text_size::{Ranged, TextRange};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn hover_basic() {
|
||||||
|
let test = cursor_test(
|
||||||
|
r#"
|
||||||
|
a = 10
|
||||||
|
|
||||||
|
a<CURSOR>
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_snapshot!(test.hover(), @r"
|
||||||
|
Literal[10]
|
||||||
|
---------------------------------------------
|
||||||
|
```text
|
||||||
|
Literal[10]
|
||||||
|
```
|
||||||
|
---------------------------------------------
|
||||||
|
info: lint:hover: Hovered content is
|
||||||
|
--> /main.py:4:9
|
||||||
|
|
|
||||||
|
2 | a = 10
|
||||||
|
3 |
|
||||||
|
4 | a
|
||||||
|
| ^- Cursor offset
|
||||||
|
| |
|
||||||
|
| source
|
||||||
|
|
|
||||||
|
");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn hover_member() {
|
||||||
|
let test = cursor_test(
|
||||||
|
r#"
|
||||||
|
class Foo:
|
||||||
|
a: int = 10
|
||||||
|
|
||||||
|
def __init__(a: int, b: str):
|
||||||
|
self.a = a
|
||||||
|
self.b: str = b
|
||||||
|
|
||||||
|
foo = Foo()
|
||||||
|
foo.<CURSOR>a
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_snapshot!(test.hover(), @r"
|
||||||
|
int
|
||||||
|
---------------------------------------------
|
||||||
|
```text
|
||||||
|
int
|
||||||
|
```
|
||||||
|
---------------------------------------------
|
||||||
|
info: lint:hover: Hovered content is
|
||||||
|
--> /main.py:10:9
|
||||||
|
|
|
||||||
|
9 | foo = Foo()
|
||||||
|
10 | foo.a
|
||||||
|
| ^^^^-
|
||||||
|
| | |
|
||||||
|
| | Cursor offset
|
||||||
|
| source
|
||||||
|
|
|
||||||
|
");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn hover_function_typed_variable() {
|
||||||
|
let test = cursor_test(
|
||||||
|
r#"
|
||||||
|
def foo(a, b): ...
|
||||||
|
|
||||||
|
foo<CURSOR>
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_snapshot!(test.hover(), @r"
|
||||||
|
Literal[foo]
|
||||||
|
---------------------------------------------
|
||||||
|
```text
|
||||||
|
Literal[foo]
|
||||||
|
```
|
||||||
|
---------------------------------------------
|
||||||
|
info: lint:hover: Hovered content is
|
||||||
|
--> /main.py:4:13
|
||||||
|
|
|
||||||
|
2 | def foo(a, b): ...
|
||||||
|
3 |
|
||||||
|
4 | foo
|
||||||
|
| ^^^- Cursor offset
|
||||||
|
| |
|
||||||
|
| source
|
||||||
|
|
|
||||||
|
");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn hover_binary_expression() {
|
||||||
|
let test = cursor_test(
|
||||||
|
r#"
|
||||||
|
def foo(a: int, b: int, c: int):
|
||||||
|
a + b ==<CURSOR> c
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_snapshot!(test.hover(), @r"
|
||||||
|
bool
|
||||||
|
---------------------------------------------
|
||||||
|
```text
|
||||||
|
bool
|
||||||
|
```
|
||||||
|
---------------------------------------------
|
||||||
|
info: lint:hover: Hovered content is
|
||||||
|
--> /main.py:3:17
|
||||||
|
|
|
||||||
|
2 | def foo(a: int, b: int, c: int):
|
||||||
|
3 | a + b == c
|
||||||
|
| ^^^^^^^^-^
|
||||||
|
| | |
|
||||||
|
| | Cursor offset
|
||||||
|
| source
|
||||||
|
|
|
||||||
|
");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn hover_keyword_parameter() {
|
||||||
|
let test = cursor_test(
|
||||||
|
r#"
|
||||||
|
def test(a: int): ...
|
||||||
|
|
||||||
|
test(a<CURSOR>= 123)
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
|
||||||
|
// TODO: This should reveal `int` because the user hovers over the parameter and not the value.
|
||||||
|
assert_snapshot!(test.hover(), @r"
|
||||||
|
Literal[123]
|
||||||
|
---------------------------------------------
|
||||||
|
```text
|
||||||
|
Literal[123]
|
||||||
|
```
|
||||||
|
---------------------------------------------
|
||||||
|
info: lint:hover: Hovered content is
|
||||||
|
--> /main.py:4:18
|
||||||
|
|
|
||||||
|
2 | def test(a: int): ...
|
||||||
|
3 |
|
||||||
|
4 | test(a= 123)
|
||||||
|
| ^- Cursor offset
|
||||||
|
| |
|
||||||
|
| source
|
||||||
|
|
|
||||||
|
");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn hover_union() {
|
||||||
|
let test = cursor_test(
|
||||||
|
r#"
|
||||||
|
|
||||||
|
def foo(a, b): ...
|
||||||
|
|
||||||
|
def bar(a, b): ...
|
||||||
|
|
||||||
|
if random.choice([True, False]):
|
||||||
|
a = foo
|
||||||
|
else:
|
||||||
|
a = bar
|
||||||
|
|
||||||
|
a<CURSOR>
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_snapshot!(test.hover(), @r"
|
||||||
|
Literal[foo, bar]
|
||||||
|
---------------------------------------------
|
||||||
|
```text
|
||||||
|
Literal[foo, bar]
|
||||||
|
```
|
||||||
|
---------------------------------------------
|
||||||
|
info: lint:hover: Hovered content is
|
||||||
|
--> /main.py:12:13
|
||||||
|
|
|
||||||
|
10 | a = bar
|
||||||
|
11 |
|
||||||
|
12 | a
|
||||||
|
| ^- Cursor offset
|
||||||
|
| |
|
||||||
|
| source
|
||||||
|
|
|
||||||
|
");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn hover_module() {
|
||||||
|
let mut test = cursor_test(
|
||||||
|
r#"
|
||||||
|
import lib
|
||||||
|
|
||||||
|
li<CURSOR>b
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
|
||||||
|
test.write_file("lib.py", "a = 10").unwrap();
|
||||||
|
|
||||||
|
assert_snapshot!(test.hover(), @r"
|
||||||
|
<module 'lib'>
|
||||||
|
---------------------------------------------
|
||||||
|
```text
|
||||||
|
<module 'lib'>
|
||||||
|
```
|
||||||
|
---------------------------------------------
|
||||||
|
info: lint:hover: Hovered content is
|
||||||
|
--> /main.py:4:13
|
||||||
|
|
|
||||||
|
2 | import lib
|
||||||
|
3 |
|
||||||
|
4 | lib
|
||||||
|
| ^^-
|
||||||
|
| | |
|
||||||
|
| | Cursor offset
|
||||||
|
| source
|
||||||
|
|
|
||||||
|
");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn hover_type_of_expression_with_type_var_type() {
|
||||||
|
let test = cursor_test(
|
||||||
|
r#"
|
||||||
|
type Alias[T: int = bool] = list[T<CURSOR>]
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_snapshot!(test.hover(), @r"
|
||||||
|
T
|
||||||
|
---------------------------------------------
|
||||||
|
```text
|
||||||
|
T
|
||||||
|
```
|
||||||
|
---------------------------------------------
|
||||||
|
info: lint:hover: Hovered content is
|
||||||
|
--> /main.py:2:46
|
||||||
|
|
|
||||||
|
2 | type Alias[T: int = bool] = list[T]
|
||||||
|
| ^- Cursor offset
|
||||||
|
| |
|
||||||
|
| source
|
||||||
|
|
|
||||||
|
");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn hover_type_of_expression_with_type_param_spec() {
|
||||||
|
let test = cursor_test(
|
||||||
|
r#"
|
||||||
|
type Alias[**P = [int, str]] = Callable[P<CURSOR>, int]
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_snapshot!(test.hover(), @r"
|
||||||
|
@Todo
|
||||||
|
---------------------------------------------
|
||||||
|
```text
|
||||||
|
@Todo
|
||||||
|
```
|
||||||
|
---------------------------------------------
|
||||||
|
info: lint:hover: Hovered content is
|
||||||
|
--> /main.py:2:53
|
||||||
|
|
|
||||||
|
2 | type Alias[**P = [int, str]] = Callable[P, int]
|
||||||
|
| ^- Cursor offset
|
||||||
|
| |
|
||||||
|
| source
|
||||||
|
|
|
||||||
|
");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn hover_type_of_expression_with_type_var_tuple() {
|
||||||
|
let test = cursor_test(
|
||||||
|
r#"
|
||||||
|
type Alias[*Ts = ()] = tuple[*Ts<CURSOR>]
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_snapshot!(test.hover(), @r"
|
||||||
|
@Todo
|
||||||
|
---------------------------------------------
|
||||||
|
```text
|
||||||
|
@Todo
|
||||||
|
```
|
||||||
|
---------------------------------------------
|
||||||
|
info: lint:hover: Hovered content is
|
||||||
|
--> /main.py:2:43
|
||||||
|
|
|
||||||
|
2 | type Alias[*Ts = ()] = tuple[*Ts]
|
||||||
|
| ^^- Cursor offset
|
||||||
|
| |
|
||||||
|
| source
|
||||||
|
|
|
||||||
|
");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn hover_class_member_declaration() {
|
||||||
|
let test = cursor_test(
|
||||||
|
r#"
|
||||||
|
class Foo:
|
||||||
|
a<CURSOR>: int
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
|
||||||
|
// TODO: This should be int and not `Never`, https://github.com/astral-sh/ruff/issues/17122
|
||||||
|
assert_snapshot!(test.hover(), @r"
|
||||||
|
Never
|
||||||
|
---------------------------------------------
|
||||||
|
```text
|
||||||
|
Never
|
||||||
|
```
|
||||||
|
---------------------------------------------
|
||||||
|
info: lint:hover: Hovered content is
|
||||||
|
--> /main.py:3:13
|
||||||
|
|
|
||||||
|
2 | class Foo:
|
||||||
|
3 | a: int
|
||||||
|
| ^- Cursor offset
|
||||||
|
| |
|
||||||
|
| source
|
||||||
|
|
|
||||||
|
");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn hover_type_narrowing() {
|
||||||
|
let test = cursor_test(
|
||||||
|
r#"
|
||||||
|
def foo(a: str | None, b):
|
||||||
|
if a is not None:
|
||||||
|
print(a<CURSOR>)
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_snapshot!(test.hover(), @r"
|
||||||
|
str
|
||||||
|
---------------------------------------------
|
||||||
|
```text
|
||||||
|
str
|
||||||
|
```
|
||||||
|
---------------------------------------------
|
||||||
|
info: lint:hover: Hovered content is
|
||||||
|
--> /main.py:4:27
|
||||||
|
|
|
||||||
|
2 | def foo(a: str | None, b):
|
||||||
|
3 | if a is not None:
|
||||||
|
4 | print(a)
|
||||||
|
| ^- Cursor offset
|
||||||
|
| |
|
||||||
|
| source
|
||||||
|
|
|
||||||
|
");
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CursorTest {
|
||||||
|
fn hover(&self) -> String {
|
||||||
|
use std::fmt::Write;
|
||||||
|
|
||||||
|
let Some(hover) = hover(&self.db, self.file, self.cursor_offset) else {
|
||||||
|
return "Hover provided no content".to_string();
|
||||||
|
};
|
||||||
|
|
||||||
|
let source = hover.range;
|
||||||
|
|
||||||
|
let mut buf = String::new();
|
||||||
|
|
||||||
|
write!(
|
||||||
|
&mut buf,
|
||||||
|
"{plaintext}{line}{markdown}{line}",
|
||||||
|
plaintext = hover.display(&self.db, MarkupKind::PlainText),
|
||||||
|
line = MarkupKind::PlainText.horizontal_line(),
|
||||||
|
markdown = hover.display(&self.db, MarkupKind::Markdown),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let config = DisplayDiagnosticConfig::default()
|
||||||
|
.color(false)
|
||||||
|
.format(DiagnosticFormat::Full);
|
||||||
|
|
||||||
|
let mut diagnostic = Diagnostic::new(
|
||||||
|
DiagnosticId::Lint(LintName::of("hover")),
|
||||||
|
Severity::Info,
|
||||||
|
"Hovered content is",
|
||||||
|
);
|
||||||
|
diagnostic.annotate(
|
||||||
|
Annotation::primary(Span::from(source.file()).with_range(source.range()))
|
||||||
|
.message("source"),
|
||||||
|
);
|
||||||
|
diagnostic.annotate(
|
||||||
|
Annotation::secondary(
|
||||||
|
Span::from(source.file()).with_range(TextRange::empty(self.cursor_offset)),
|
||||||
|
)
|
||||||
|
.message("Cursor offset"),
|
||||||
|
);
|
||||||
|
|
||||||
|
write!(buf, "{}", diagnostic.display(&self.db, &config)).unwrap();
|
||||||
|
|
||||||
|
buf
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,11 +1,15 @@
|
||||||
mod db;
|
mod db;
|
||||||
mod find_node;
|
mod find_node;
|
||||||
mod goto;
|
mod goto;
|
||||||
|
mod hover;
|
||||||
|
mod markup;
|
||||||
|
|
||||||
use std::ops::{Deref, DerefMut};
|
use std::ops::{Deref, DerefMut};
|
||||||
|
|
||||||
pub use db::Db;
|
pub use db::Db;
|
||||||
pub use goto::goto_type_definition;
|
pub use goto::goto_type_definition;
|
||||||
|
pub use hover::hover;
|
||||||
|
pub use markup::MarkupKind;
|
||||||
use red_knot_python_semantic::types::{
|
use red_knot_python_semantic::types::{
|
||||||
Class, ClassBase, ClassLiteralType, FunctionType, InstanceType, IntersectionType,
|
Class, ClassBase, ClassLiteralType, FunctionType, InstanceType, IntersectionType,
|
||||||
KnownInstanceType, ModuleLiteralType, Type,
|
KnownInstanceType, ModuleLiteralType, Type,
|
||||||
|
@ -272,3 +276,103 @@ impl HasNavigationTargets for IntersectionType<'_> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::db::tests::TestDb;
|
||||||
|
use insta::internals::SettingsBindDropGuard;
|
||||||
|
use red_knot_python_semantic::{
|
||||||
|
Program, ProgramSettings, PythonPath, PythonPlatform, SearchPathSettings,
|
||||||
|
};
|
||||||
|
use ruff_db::diagnostic::{Diagnostic, DiagnosticFormat, DisplayDiagnosticConfig};
|
||||||
|
use ruff_db::files::{system_path_to_file, File};
|
||||||
|
use ruff_db::system::{DbWithWritableSystem, SystemPath, SystemPathBuf};
|
||||||
|
use ruff_python_ast::PythonVersion;
|
||||||
|
use ruff_text_size::TextSize;
|
||||||
|
|
||||||
|
pub(super) fn cursor_test(source: &str) -> CursorTest {
|
||||||
|
let mut db = TestDb::new();
|
||||||
|
let cursor_offset = source.find("<CURSOR>").expect(
|
||||||
|
"`source`` should contain a `<CURSOR>` marker, indicating the position of the cursor.",
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut content = source[..cursor_offset].to_string();
|
||||||
|
content.push_str(&source[cursor_offset + "<CURSOR>".len()..]);
|
||||||
|
|
||||||
|
db.write_file("main.py", &content)
|
||||||
|
.expect("write to memory file system to be successful");
|
||||||
|
|
||||||
|
let file = system_path_to_file(&db, "main.py").expect("newly written file to existing");
|
||||||
|
|
||||||
|
Program::from_settings(
|
||||||
|
&db,
|
||||||
|
ProgramSettings {
|
||||||
|
python_version: PythonVersion::latest(),
|
||||||
|
python_platform: PythonPlatform::default(),
|
||||||
|
search_paths: SearchPathSettings {
|
||||||
|
extra_paths: vec![],
|
||||||
|
src_roots: vec![SystemPathBuf::from("/")],
|
||||||
|
custom_typeshed: None,
|
||||||
|
python_path: PythonPath::KnownSitePackages(vec![]),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.expect("Default settings to be valid");
|
||||||
|
|
||||||
|
let mut insta_settings = insta::Settings::clone_current();
|
||||||
|
insta_settings.add_filter(r#"\\(\w\w|\s|\.|")"#, "/$1");
|
||||||
|
// Filter out TODO types because they are different between debug and release builds.
|
||||||
|
insta_settings.add_filter(r"@Todo\(.+\)", "@Todo");
|
||||||
|
|
||||||
|
let insta_settings_guard = insta_settings.bind_to_scope();
|
||||||
|
|
||||||
|
CursorTest {
|
||||||
|
db,
|
||||||
|
cursor_offset: TextSize::try_from(cursor_offset)
|
||||||
|
.expect("source to be smaller than 4GB"),
|
||||||
|
file,
|
||||||
|
_insta_settings_guard: insta_settings_guard,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) struct CursorTest {
|
||||||
|
pub(super) db: TestDb,
|
||||||
|
pub(super) cursor_offset: TextSize,
|
||||||
|
pub(super) file: File,
|
||||||
|
_insta_settings_guard: SettingsBindDropGuard,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CursorTest {
|
||||||
|
pub(super) fn write_file(
|
||||||
|
&mut self,
|
||||||
|
path: impl AsRef<SystemPath>,
|
||||||
|
content: &str,
|
||||||
|
) -> std::io::Result<()> {
|
||||||
|
self.db.write_file(path, content)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn render_diagnostics<I, D>(&self, diagnostics: I) -> String
|
||||||
|
where
|
||||||
|
I: IntoIterator<Item = D>,
|
||||||
|
D: IntoDiagnostic,
|
||||||
|
{
|
||||||
|
use std::fmt::Write;
|
||||||
|
|
||||||
|
let mut buf = String::new();
|
||||||
|
|
||||||
|
let config = DisplayDiagnosticConfig::default()
|
||||||
|
.color(false)
|
||||||
|
.format(DiagnosticFormat::Full);
|
||||||
|
for diagnostic in diagnostics {
|
||||||
|
let diag = diagnostic.into_diagnostic();
|
||||||
|
write!(buf, "{}", diag.display(&self.db, &config)).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
buf
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) trait IntoDiagnostic {
|
||||||
|
fn into_diagnostic(self) -> Diagnostic;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
66
crates/red_knot_ide/src/markup.rs
Normal file
66
crates/red_knot_ide/src/markup.rs
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
use std::fmt;
|
||||||
|
use std::fmt::Formatter;
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
|
||||||
|
pub enum MarkupKind {
|
||||||
|
PlainText,
|
||||||
|
Markdown,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MarkupKind {
|
||||||
|
pub(crate) const fn fenced_code_block<T>(self, code: T, language: &str) -> FencedCodeBlock<T>
|
||||||
|
where
|
||||||
|
T: fmt::Display,
|
||||||
|
{
|
||||||
|
FencedCodeBlock {
|
||||||
|
language,
|
||||||
|
code,
|
||||||
|
kind: self,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) const fn horizontal_line(self) -> HorizontalLine {
|
||||||
|
HorizontalLine { kind: self }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct FencedCodeBlock<'a, T> {
|
||||||
|
language: &'a str,
|
||||||
|
code: T,
|
||||||
|
kind: MarkupKind,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> fmt::Display for FencedCodeBlock<'_, T>
|
||||||
|
where
|
||||||
|
T: fmt::Display,
|
||||||
|
{
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self.kind {
|
||||||
|
MarkupKind::PlainText => self.code.fmt(f),
|
||||||
|
MarkupKind::Markdown => write!(
|
||||||
|
f,
|
||||||
|
"```{language}\n{code}\n```",
|
||||||
|
language = self.language,
|
||||||
|
code = self.code
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone)]
|
||||||
|
pub(crate) struct HorizontalLine {
|
||||||
|
kind: MarkupKind,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for HorizontalLine {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||||
|
match self.kind {
|
||||||
|
MarkupKind::PlainText => {
|
||||||
|
f.write_str("\n---------------------------------------------\n")
|
||||||
|
}
|
||||||
|
MarkupKind::Markdown => {
|
||||||
|
write!(f, "\n---\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -38,14 +38,14 @@ impl ToLink for NavigationTarget {
|
||||||
let source = source_text(db.upcast(), file);
|
let source = source_text(db.upcast(), file);
|
||||||
let index = line_index(db.upcast(), file);
|
let index = line_index(db.upcast(), file);
|
||||||
|
|
||||||
let target_range = self.full_range().to_range(&source, &index, encoding);
|
let target_range = self.full_range().to_lsp_range(&source, &index, encoding);
|
||||||
let selection_range = self.focus_range().to_range(&source, &index, encoding);
|
let selection_range = self.focus_range().to_lsp_range(&source, &index, encoding);
|
||||||
|
|
||||||
let src = src.map(|src| {
|
let src = src.map(|src| {
|
||||||
let source = source_text(db.upcast(), src.file());
|
let source = source_text(db.upcast(), src.file());
|
||||||
let index = line_index(db.upcast(), src.file());
|
let index = line_index(db.upcast(), src.file());
|
||||||
|
|
||||||
src.range().to_range(&source, &index, encoding)
|
src.range().to_lsp_range(&source, &index, encoding)
|
||||||
});
|
});
|
||||||
|
|
||||||
Some(lsp_types::LocationLink {
|
Some(lsp_types::LocationLink {
|
||||||
|
|
|
@ -28,7 +28,12 @@ pub(crate) trait PositionExt {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) trait ToRangeExt {
|
pub(crate) trait ToRangeExt {
|
||||||
fn to_range(&self, text: &str, index: &LineIndex, encoding: PositionEncoding) -> types::Range;
|
fn to_lsp_range(
|
||||||
|
&self,
|
||||||
|
text: &str,
|
||||||
|
index: &LineIndex,
|
||||||
|
encoding: PositionEncoding,
|
||||||
|
) -> types::Range;
|
||||||
fn to_notebook_range(
|
fn to_notebook_range(
|
||||||
&self,
|
&self,
|
||||||
text: &str,
|
text: &str,
|
||||||
|
@ -92,7 +97,12 @@ impl RangeExt for lsp_types::Range {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ToRangeExt for TextRange {
|
impl ToRangeExt for TextRange {
|
||||||
fn to_range(&self, text: &str, index: &LineIndex, encoding: PositionEncoding) -> types::Range {
|
fn to_lsp_range(
|
||||||
|
&self,
|
||||||
|
text: &str,
|
||||||
|
index: &LineIndex,
|
||||||
|
encoding: PositionEncoding,
|
||||||
|
) -> types::Range {
|
||||||
types::Range {
|
types::Range {
|
||||||
start: source_location_to_position(&offset_to_source_location(
|
start: source_location_to_position(&offset_to_source_location(
|
||||||
self.start(),
|
self.start(),
|
||||||
|
@ -222,7 +232,7 @@ impl FileRangeExt for FileRange {
|
||||||
let source = source_text(db.upcast(), file);
|
let source = source_text(db.upcast(), file);
|
||||||
let line_index = line_index(db.upcast(), file);
|
let line_index = line_index(db.upcast(), file);
|
||||||
|
|
||||||
let range = self.range().to_range(&source, &line_index, encoding);
|
let range = self.range().to_lsp_range(&source, &line_index, encoding);
|
||||||
Some(Location { uri, range })
|
Some(Location { uri, range })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,9 +7,9 @@ use std::panic::PanicInfo;
|
||||||
|
|
||||||
use lsp_server::Message;
|
use lsp_server::Message;
|
||||||
use lsp_types::{
|
use lsp_types::{
|
||||||
ClientCapabilities, DiagnosticOptions, DiagnosticServerCapabilities, MessageType,
|
ClientCapabilities, DiagnosticOptions, DiagnosticServerCapabilities, HoverProviderCapability,
|
||||||
ServerCapabilities, TextDocumentSyncCapability, TextDocumentSyncKind, TextDocumentSyncOptions,
|
MessageType, ServerCapabilities, TextDocumentSyncCapability, TextDocumentSyncKind,
|
||||||
TypeDefinitionProviderCapability, Url,
|
TextDocumentSyncOptions, TypeDefinitionProviderCapability, Url,
|
||||||
};
|
};
|
||||||
|
|
||||||
use self::connection::{Connection, ConnectionInitializer};
|
use self::connection::{Connection, ConnectionInitializer};
|
||||||
|
@ -221,6 +221,7 @@ impl Server {
|
||||||
},
|
},
|
||||||
)),
|
)),
|
||||||
type_definition_provider: Some(TypeDefinitionProviderCapability::Simple(true)),
|
type_definition_provider: Some(TypeDefinitionProviderCapability::Simple(true)),
|
||||||
|
hover_provider: Some(HoverProviderCapability::Simple(true)),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,6 +32,10 @@ pub(super) fn request<'a>(req: server::Request) -> Task<'a> {
|
||||||
BackgroundSchedule::LatencySensitive,
|
BackgroundSchedule::LatencySensitive,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
request::HoverRequestHandler::METHOD => background_request_task::<
|
||||||
|
request::HoverRequestHandler,
|
||||||
|
>(req, BackgroundSchedule::LatencySensitive),
|
||||||
|
|
||||||
method => {
|
method => {
|
||||||
tracing::warn!("Received request {method} which does not have a handler");
|
tracing::warn!("Received request {method} which does not have a handler");
|
||||||
return Task::nothing();
|
return Task::nothing();
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
mod diagnostic;
|
mod diagnostic;
|
||||||
mod goto_type_definition;
|
mod goto_type_definition;
|
||||||
|
mod hover;
|
||||||
|
|
||||||
pub(super) use diagnostic::DocumentDiagnosticRequestHandler;
|
pub(super) use diagnostic::DocumentDiagnosticRequestHandler;
|
||||||
pub(super) use goto_type_definition::GotoTypeDefinitionRequestHandler;
|
pub(super) use goto_type_definition::GotoTypeDefinitionRequestHandler;
|
||||||
|
pub(super) use hover::HoverRequestHandler;
|
||||||
|
|
|
@ -80,7 +80,7 @@ fn to_lsp_diagnostic(
|
||||||
let source = source_text(db.upcast(), span.file());
|
let source = source_text(db.upcast(), span.file());
|
||||||
|
|
||||||
span.range()
|
span.range()
|
||||||
.map(|range| range.to_range(&source, &index, encoding))
|
.map(|range| range.to_lsp_range(&source, &index, encoding))
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
} else {
|
} else {
|
||||||
Range::default()
|
Range::default()
|
||||||
|
|
71
crates/red_knot_server/src/server/api/requests/hover.rs
Normal file
71
crates/red_knot_server/src/server/api/requests/hover.rs
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
use std::borrow::Cow;
|
||||||
|
|
||||||
|
use crate::document::{PositionExt, ToRangeExt};
|
||||||
|
use crate::server::api::traits::{BackgroundDocumentRequestHandler, RequestHandler};
|
||||||
|
use crate::server::client::Notifier;
|
||||||
|
use crate::DocumentSnapshot;
|
||||||
|
use lsp_types::request::HoverRequest;
|
||||||
|
use lsp_types::{HoverContents, HoverParams, MarkupContent, Url};
|
||||||
|
use red_knot_ide::{hover, MarkupKind};
|
||||||
|
use red_knot_project::ProjectDatabase;
|
||||||
|
use ruff_db::source::{line_index, source_text};
|
||||||
|
use ruff_text_size::Ranged;
|
||||||
|
|
||||||
|
pub(crate) struct HoverRequestHandler;
|
||||||
|
|
||||||
|
impl RequestHandler for HoverRequestHandler {
|
||||||
|
type RequestType = HoverRequest;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BackgroundDocumentRequestHandler for HoverRequestHandler {
|
||||||
|
fn document_url(params: &HoverParams) -> Cow<Url> {
|
||||||
|
Cow::Borrowed(¶ms.text_document_position_params.text_document.uri)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_with_snapshot(
|
||||||
|
snapshot: DocumentSnapshot,
|
||||||
|
db: ProjectDatabase,
|
||||||
|
_notifier: Notifier,
|
||||||
|
params: HoverParams,
|
||||||
|
) -> crate::server::Result<Option<lsp_types::Hover>> {
|
||||||
|
let Some(file) = snapshot.file(&db) else {
|
||||||
|
tracing::debug!("Failed to resolve file for {:?}", params);
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
|
||||||
|
let source = source_text(&db, file);
|
||||||
|
let line_index = line_index(&db, file);
|
||||||
|
let offset = params.text_document_position_params.position.to_text_size(
|
||||||
|
&source,
|
||||||
|
&line_index,
|
||||||
|
snapshot.encoding(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let Some(range_info) = hover(&db, file, offset) else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
|
||||||
|
let (markup_kind, lsp_markup_kind) = if snapshot
|
||||||
|
.resolved_client_capabilities()
|
||||||
|
.hover_prefer_markdown
|
||||||
|
{
|
||||||
|
(MarkupKind::Markdown, lsp_types::MarkupKind::Markdown)
|
||||||
|
} else {
|
||||||
|
(MarkupKind::PlainText, lsp_types::MarkupKind::PlainText)
|
||||||
|
};
|
||||||
|
|
||||||
|
let contents = range_info.display(&db, markup_kind).to_string();
|
||||||
|
|
||||||
|
Ok(Some(lsp_types::Hover {
|
||||||
|
contents: HoverContents::Markup(MarkupContent {
|
||||||
|
kind: lsp_markup_kind,
|
||||||
|
value: contents,
|
||||||
|
}),
|
||||||
|
range: Some(range_info.file_range().range().to_lsp_range(
|
||||||
|
&source,
|
||||||
|
&line_index,
|
||||||
|
snapshot.encoding(),
|
||||||
|
)),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
use lsp_types::ClientCapabilities;
|
use lsp_types::{ClientCapabilities, MarkupKind};
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||||
#[allow(clippy::struct_excessive_bools)]
|
#[allow(clippy::struct_excessive_bools)]
|
||||||
|
@ -10,6 +10,9 @@ pub(crate) struct ResolvedClientCapabilities {
|
||||||
pub(crate) pull_diagnostics: bool,
|
pub(crate) pull_diagnostics: bool,
|
||||||
/// Whether `textDocument.typeDefinition.linkSupport` is `true`
|
/// Whether `textDocument.typeDefinition.linkSupport` is `true`
|
||||||
pub(crate) type_definition_link_support: bool,
|
pub(crate) type_definition_link_support: bool,
|
||||||
|
|
||||||
|
/// `true`, if the first markup kind in `textDocument.hover.contentFormat` is `Markdown`
|
||||||
|
pub(crate) hover_prefer_markdown: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ResolvedClientCapabilities {
|
impl ResolvedClientCapabilities {
|
||||||
|
@ -63,6 +66,21 @@ impl ResolvedClientCapabilities {
|
||||||
.and_then(|text_document| text_document.diagnostic.as_ref())
|
.and_then(|text_document| text_document.diagnostic.as_ref())
|
||||||
.is_some();
|
.is_some();
|
||||||
|
|
||||||
|
let hover_prefer_markdown = client_capabilities
|
||||||
|
.text_document
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|text_document| {
|
||||||
|
Some(
|
||||||
|
text_document
|
||||||
|
.hover
|
||||||
|
.as_ref()?
|
||||||
|
.content_format
|
||||||
|
.as_ref()?
|
||||||
|
.contains(&MarkupKind::Markdown),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
code_action_deferred_edit_resolution: code_action_data_support
|
code_action_deferred_edit_resolution: code_action_data_support
|
||||||
&& code_action_edit_resolution,
|
&& code_action_edit_resolution,
|
||||||
|
@ -71,6 +89,7 @@ impl ResolvedClientCapabilities {
|
||||||
workspace_refresh,
|
workspace_refresh,
|
||||||
pull_diagnostics,
|
pull_diagnostics,
|
||||||
type_definition_link_support: declaration_link_support,
|
type_definition_link_support: declaration_link_support,
|
||||||
|
hover_prefer_markdown,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use std::any::Any;
|
use std::any::Any;
|
||||||
|
|
||||||
use js_sys::{Error, JsString};
|
use js_sys::{Error, JsString};
|
||||||
use red_knot_ide::goto_type_definition;
|
use red_knot_ide::{goto_type_definition, hover, MarkupKind};
|
||||||
use red_knot_project::metadata::options::Options;
|
use red_knot_project::metadata::options::Options;
|
||||||
use red_knot_project::metadata::value::ValueSource;
|
use red_knot_project::metadata::value::ValueSource;
|
||||||
use red_knot_project::watch::{ChangeEvent, ChangedKind, CreatedKind, DeletedKind};
|
use red_knot_project::watch::{ChangeEvent, ChangedKind, CreatedKind, DeletedKind};
|
||||||
|
@ -243,6 +243,37 @@ impl Workspace {
|
||||||
|
|
||||||
Ok(links)
|
Ok(links)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn hover(&self, file_id: &FileHandle, position: Position) -> Result<Option<Hover>, Error> {
|
||||||
|
let source = source_text(&self.db, file_id.file);
|
||||||
|
let index = line_index(&self.db, file_id.file);
|
||||||
|
|
||||||
|
let offset = index.offset(
|
||||||
|
OneIndexed::new(position.line).ok_or_else(|| {
|
||||||
|
Error::new("Invalid value `0` for `position.line`. The line index is 1-indexed.")
|
||||||
|
})?,
|
||||||
|
OneIndexed::new(position.column).ok_or_else(|| {
|
||||||
|
Error::new(
|
||||||
|
"Invalid value `0` for `position.column`. The column index is 1-indexed.",
|
||||||
|
)
|
||||||
|
})?,
|
||||||
|
&source,
|
||||||
|
);
|
||||||
|
|
||||||
|
let Some(range_info) = hover(&self.db, file_id.file, offset) else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
|
||||||
|
let source_range = Range::from_text_range(range_info.file_range().range(), &index, &source);
|
||||||
|
|
||||||
|
Ok(Some(Hover {
|
||||||
|
markdown: range_info
|
||||||
|
.display(&self.db, MarkupKind::Markdown)
|
||||||
|
.to_string(),
|
||||||
|
range: source_range,
|
||||||
|
}))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn into_error<E: std::fmt::Display>(err: E) -> Error {
|
pub(crate) fn into_error<E: std::fmt::Display>(err: E) -> Error {
|
||||||
|
@ -330,15 +361,11 @@ pub struct Range {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Range {
|
impl Range {
|
||||||
fn from_file_range(db: &dyn Db, range: FileRange) -> Self {
|
fn from_file_range(db: &dyn Db, file_range: FileRange) -> Self {
|
||||||
let index = line_index(db.upcast(), range.file());
|
let index = line_index(db.upcast(), file_range.file());
|
||||||
let source = source_text(db.upcast(), range.file());
|
let source = source_text(db.upcast(), file_range.file());
|
||||||
|
|
||||||
let text_range = range.range();
|
Self::from_text_range(file_range.range(), &index, &source)
|
||||||
|
|
||||||
let start = index.source_location(text_range.start(), &source);
|
|
||||||
let end = index.source_location(text_range.end(), &source);
|
|
||||||
Self::from((start, end))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn from_text_range(
|
fn from_text_range(
|
||||||
|
@ -437,6 +464,14 @@ pub struct LocationLink {
|
||||||
pub origin_selection_range: Option<Range>,
|
pub origin_selection_range: Option<Range>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub struct Hover {
|
||||||
|
#[wasm_bindgen(getter_with_clone)]
|
||||||
|
pub markdown: String,
|
||||||
|
|
||||||
|
pub range: Range,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
struct WasmSystem {
|
struct WasmSystem {
|
||||||
fs: MemoryFileSystem,
|
fs: MemoryFileSystem,
|
||||||
|
|
|
@ -690,6 +690,7 @@ impl Deref for Tokens {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A token that encloses a given offset or ends exactly at it.
|
/// A token that encloses a given offset or ends exactly at it.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
pub enum TokenAt {
|
pub enum TokenAt {
|
||||||
/// There's no token at the given offset
|
/// There's no token at the given offset
|
||||||
None,
|
None,
|
||||||
|
|
|
@ -56,6 +56,7 @@ export default function Editor({
|
||||||
const disposable = useRef<{
|
const disposable = useRef<{
|
||||||
typeDefinition: IDisposable;
|
typeDefinition: IDisposable;
|
||||||
editorOpener: IDisposable;
|
editorOpener: IDisposable;
|
||||||
|
hover: IDisposable;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
const playgroundState = useRef<PlaygroundServerProps>({
|
const playgroundState = useRef<PlaygroundServerProps>({
|
||||||
monaco: null,
|
monaco: null,
|
||||||
|
@ -93,6 +94,7 @@ export default function Editor({
|
||||||
return () => {
|
return () => {
|
||||||
disposable.current?.typeDefinition.dispose();
|
disposable.current?.typeDefinition.dispose();
|
||||||
disposable.current?.editorOpener.dispose();
|
disposable.current?.editorOpener.dispose();
|
||||||
|
disposable.current?.hover.dispose();
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
@ -103,12 +105,17 @@ export default function Editor({
|
||||||
const server = new PlaygroundServer(playgroundState);
|
const server = new PlaygroundServer(playgroundState);
|
||||||
const typeDefinitionDisposable =
|
const typeDefinitionDisposable =
|
||||||
instance.languages.registerTypeDefinitionProvider("python", server);
|
instance.languages.registerTypeDefinitionProvider("python", server);
|
||||||
|
const hoverDisposable = instance.languages.registerHoverProvider(
|
||||||
|
"python",
|
||||||
|
server,
|
||||||
|
);
|
||||||
const editorOpenerDisposable =
|
const editorOpenerDisposable =
|
||||||
instance.editor.registerEditorOpener(server);
|
instance.editor.registerEditorOpener(server);
|
||||||
|
|
||||||
disposable.current = {
|
disposable.current = {
|
||||||
typeDefinition: typeDefinitionDisposable,
|
typeDefinition: typeDefinitionDisposable,
|
||||||
editorOpener: editorOpenerDisposable,
|
editorOpener: editorOpenerDisposable,
|
||||||
|
hover: hoverDisposable,
|
||||||
};
|
};
|
||||||
|
|
||||||
playgroundState.current.monaco = instance;
|
playgroundState.current.monaco = instance;
|
||||||
|
@ -191,10 +198,49 @@ interface PlaygroundServerProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
class PlaygroundServer
|
class PlaygroundServer
|
||||||
implements languages.TypeDefinitionProvider, editor.ICodeEditorOpener
|
implements
|
||||||
|
languages.TypeDefinitionProvider,
|
||||||
|
editor.ICodeEditorOpener,
|
||||||
|
languages.HoverProvider
|
||||||
{
|
{
|
||||||
constructor(private props: RefObject<PlaygroundServerProps>) {}
|
constructor(private props: RefObject<PlaygroundServerProps>) {}
|
||||||
|
|
||||||
|
provideHover(
|
||||||
|
model: editor.ITextModel,
|
||||||
|
position: Position,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
_token: CancellationToken,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
context?: languages.HoverContext<languages.Hover> | undefined,
|
||||||
|
): languages.ProviderResult<languages.Hover> {
|
||||||
|
const workspace = this.props.current.workspace;
|
||||||
|
|
||||||
|
const selectedFile = this.props.current.files.selected;
|
||||||
|
if (selectedFile == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedHandle = this.props.current.files.handles[selectedFile];
|
||||||
|
|
||||||
|
if (selectedHandle == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hover = workspace.hover(
|
||||||
|
selectedHandle,
|
||||||
|
new KnotPosition(position.lineNumber, position.column),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (hover == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
range: knotRangeToIRange(hover.range),
|
||||||
|
contents: [{ value: hover.markdown, isTrusted: true }],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
provideTypeDefinition(
|
provideTypeDefinition(
|
||||||
model: editor.ITextModel,
|
model: editor.ITextModel,
|
||||||
position: Position,
|
position: Position,
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue