Compare commits
12 Commits
0.14.8
...
david/embe
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c3d225fe61 | ||
|
|
fdcb5a7e73 | ||
|
|
6a025d1925 | ||
|
|
f054e7edf8 | ||
|
|
e154efa229 | ||
|
|
32f400a457 | ||
|
|
2a38395bc8 | ||
|
|
8c72b296c9 | ||
|
|
086f1e0b89 | ||
|
|
5da45f8ec7 | ||
|
|
62f20b1e86 | ||
|
|
cccb0bbaa4 |
@@ -11,9 +11,9 @@ import-deprioritizes-type_check_only,main.py,2,1
|
||||
import-deprioritizes-type_check_only,main.py,3,2
|
||||
import-deprioritizes-type_check_only,main.py,4,3
|
||||
import-keyword-completion,main.py,0,1
|
||||
internal-typeshed-hidden,main.py,0,4
|
||||
internal-typeshed-hidden,main.py,0,2
|
||||
none-completion,main.py,0,2
|
||||
numpy-array,main.py,0,
|
||||
numpy-array,main.py,0,159
|
||||
numpy-array,main.py,1,1
|
||||
object-attr-instance-methods,main.py,0,1
|
||||
object-attr-instance-methods,main.py,1,1
|
||||
@@ -23,6 +23,6 @@ scope-existing-over-new-import,main.py,0,1
|
||||
scope-prioritize-closer,main.py,0,2
|
||||
scope-simple-long-identifier,main.py,0,1
|
||||
tstring-completions,main.py,0,1
|
||||
ty-extensions-lower-stdlib,main.py,0,8
|
||||
ty-extensions-lower-stdlib,main.py,0,9
|
||||
type-var-typing-over-ast,main.py,0,3
|
||||
type-var-typing-over-ast,main.py,1,275
|
||||
type-var-typing-over-ast,main.py,1,239
|
||||
|
||||
|
@@ -36,6 +36,20 @@ pub fn all_symbols<'db>(
|
||||
let Some(file) = module.file(&*db) else {
|
||||
continue;
|
||||
};
|
||||
// By convention, modules starting with an underscore
|
||||
// are generally considered unexported. However, we
|
||||
// should consider first party modules fair game.
|
||||
//
|
||||
// Note that we apply this recursively. e.g.,
|
||||
// `numpy._core.multiarray` is considered private
|
||||
// because it's a child of `_core`.
|
||||
if module.name(&*db).components().any(|c| c.starts_with('_'))
|
||||
&& module
|
||||
.search_path(&*db)
|
||||
.is_none_or(|sp| !sp.is_first_party())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
// TODO: also make it available in `TYPE_CHECKING` blocks
|
||||
// (we'd need https://github.com/astral-sh/ty/issues/1553 to do this well)
|
||||
if !is_typing_extensions_available && module.name(&*db) == &typing_extensions {
|
||||
|
||||
@@ -4350,7 +4350,7 @@ from os.<CURSOR>
|
||||
.build()
|
||||
.snapshot();
|
||||
assert_snapshot!(snapshot, @r"
|
||||
Kadabra :: Literal[1] :: Current module
|
||||
Kadabra :: Literal[1] :: <no import required>
|
||||
AbraKadabra :: Unavailable :: package
|
||||
");
|
||||
}
|
||||
@@ -5534,7 +5534,7 @@ def foo(param: s<CURSOR>)
|
||||
// Even though long_namea is alphabetically before long_nameb,
|
||||
// long_nameb is currently imported and should be preferred.
|
||||
assert_snapshot!(snapshot, @r"
|
||||
long_nameb :: Literal[1] :: Current module
|
||||
long_nameb :: Literal[1] :: <no import required>
|
||||
long_namea :: Unavailable :: foo
|
||||
");
|
||||
}
|
||||
@@ -5804,7 +5804,7 @@ from .imp<CURSOR>
|
||||
#[test]
|
||||
fn typing_extensions_excluded_from_import() {
|
||||
let builder = completion_test_builder("from typing<CURSOR>").module_names();
|
||||
assert_snapshot!(builder.build().snapshot(), @"typing :: Current module");
|
||||
assert_snapshot!(builder.build().snapshot(), @"typing :: <no import required>");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -5812,13 +5812,7 @@ from .imp<CURSOR>
|
||||
let builder = completion_test_builder("deprecated<CURSOR>")
|
||||
.auto_import()
|
||||
.module_names();
|
||||
assert_snapshot!(builder.build().snapshot(), @r"
|
||||
Deprecated :: importlib.metadata
|
||||
DeprecatedList :: importlib.metadata
|
||||
DeprecatedNonAbstract :: importlib.metadata
|
||||
DeprecatedTuple :: importlib.metadata
|
||||
deprecated :: warnings
|
||||
");
|
||||
assert_snapshot!(builder.build().snapshot(), @"deprecated :: warnings");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -5829,8 +5823,8 @@ from .imp<CURSOR>
|
||||
.completion_test_builder()
|
||||
.module_names();
|
||||
assert_snapshot!(builder.build().snapshot(), @r"
|
||||
typing :: Current module
|
||||
typing_extensions :: Current module
|
||||
typing :: <no import required>
|
||||
typing_extensions :: <no import required>
|
||||
");
|
||||
}
|
||||
|
||||
@@ -5843,10 +5837,6 @@ from .imp<CURSOR>
|
||||
.auto_import()
|
||||
.module_names();
|
||||
assert_snapshot!(builder.build().snapshot(), @r"
|
||||
Deprecated :: importlib.metadata
|
||||
DeprecatedList :: importlib.metadata
|
||||
DeprecatedNonAbstract :: importlib.metadata
|
||||
DeprecatedTuple :: importlib.metadata
|
||||
deprecated :: typing_extensions
|
||||
deprecated :: warnings
|
||||
");
|
||||
@@ -5859,8 +5849,8 @@ from .imp<CURSOR>
|
||||
.completion_test_builder()
|
||||
.module_names();
|
||||
assert_snapshot!(builder.build().snapshot(), @r"
|
||||
typing :: Current module
|
||||
typing_extensions :: Current module
|
||||
typing :: <no import required>
|
||||
typing_extensions :: <no import required>
|
||||
");
|
||||
}
|
||||
|
||||
@@ -5872,15 +5862,211 @@ from .imp<CURSOR>
|
||||
.auto_import()
|
||||
.module_names();
|
||||
assert_snapshot!(builder.build().snapshot(), @r"
|
||||
Deprecated :: importlib.metadata
|
||||
DeprecatedList :: importlib.metadata
|
||||
DeprecatedNonAbstract :: importlib.metadata
|
||||
DeprecatedTuple :: importlib.metadata
|
||||
deprecated :: typing_extensions
|
||||
deprecated :: warnings
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reexport_simple_import_noauto() {
|
||||
let snapshot = CursorTest::builder()
|
||||
.source(
|
||||
"main.py",
|
||||
r#"
|
||||
import foo
|
||||
foo.ZQ<CURSOR>
|
||||
"#,
|
||||
)
|
||||
.source("foo.py", r#"from bar import ZQZQ"#)
|
||||
.source("bar.py", r#"ZQZQ = 1"#)
|
||||
.completion_test_builder()
|
||||
.module_names()
|
||||
.build()
|
||||
.snapshot();
|
||||
assert_snapshot!(snapshot, @"ZQZQ :: <no import required>");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reexport_simple_import_auto() {
|
||||
let snapshot = CursorTest::builder()
|
||||
.source(
|
||||
"main.py",
|
||||
r#"
|
||||
ZQ<CURSOR>
|
||||
"#,
|
||||
)
|
||||
.source("foo.py", r#"from bar import ZQZQ"#)
|
||||
.source("bar.py", r#"ZQZQ = 1"#)
|
||||
.completion_test_builder()
|
||||
.auto_import()
|
||||
.module_names()
|
||||
.build()
|
||||
.snapshot();
|
||||
// We're specifically looking for `ZQZQ` in `bar`
|
||||
// here but *not* in `foo`. Namely, in `foo`,
|
||||
// `ZQZQ` is a "regular" import that is not by
|
||||
// convention considered a re-export.
|
||||
assert_snapshot!(snapshot, @"ZQZQ :: bar");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reexport_redundant_convention_import_noauto() {
|
||||
let snapshot = CursorTest::builder()
|
||||
.source(
|
||||
"main.py",
|
||||
r#"
|
||||
import foo
|
||||
foo.ZQ<CURSOR>
|
||||
"#,
|
||||
)
|
||||
.source("foo.py", r#"from bar import ZQZQ as ZQZQ"#)
|
||||
.source("bar.py", r#"ZQZQ = 1"#)
|
||||
.completion_test_builder()
|
||||
.module_names()
|
||||
.build()
|
||||
.snapshot();
|
||||
assert_snapshot!(snapshot, @"ZQZQ :: <no import required>");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reexport_redundant_convention_import_auto() {
|
||||
let snapshot = CursorTest::builder()
|
||||
.source(
|
||||
"main.py",
|
||||
r#"
|
||||
ZQ<CURSOR>
|
||||
"#,
|
||||
)
|
||||
.source("foo.py", r#"from bar import ZQZQ as ZQZQ"#)
|
||||
.source("bar.py", r#"ZQZQ = 1"#)
|
||||
.completion_test_builder()
|
||||
.auto_import()
|
||||
.module_names()
|
||||
.build()
|
||||
.snapshot();
|
||||
assert_snapshot!(snapshot, @r"
|
||||
ZQZQ :: bar
|
||||
ZQZQ :: foo
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn auto_import_respects_all() {
|
||||
let snapshot = CursorTest::builder()
|
||||
.source(
|
||||
"main.py",
|
||||
r#"
|
||||
ZQ<CURSOR>
|
||||
"#,
|
||||
)
|
||||
.source(
|
||||
"bar.py",
|
||||
r#"
|
||||
ZQZQ1 = 1
|
||||
ZQZQ2 = 1
|
||||
__all__ = ['ZQZQ1']
|
||||
"#,
|
||||
)
|
||||
.completion_test_builder()
|
||||
.auto_import()
|
||||
.module_names()
|
||||
.build()
|
||||
.snapshot();
|
||||
// We specifically do not want `ZQZQ2` here, since
|
||||
// it is not part of `__all__`.
|
||||
assert_snapshot!(snapshot, @r"
|
||||
ZQZQ1 :: bar
|
||||
");
|
||||
}
|
||||
|
||||
// This test confirms current behavior (as of 2025-12-04), but
|
||||
// it's not consistent with auto-import. That is, it doesn't
|
||||
// strictly respect `__all__` on `bar`, but perhaps it should.
|
||||
//
|
||||
// See: https://github.com/astral-sh/ty/issues/1757
|
||||
#[test]
|
||||
fn object_attr_ignores_all() {
|
||||
let snapshot = CursorTest::builder()
|
||||
.source(
|
||||
"main.py",
|
||||
r#"
|
||||
import bar
|
||||
bar.ZQ<CURSOR>
|
||||
"#,
|
||||
)
|
||||
.source(
|
||||
"bar.py",
|
||||
r#"
|
||||
ZQZQ1 = 1
|
||||
ZQZQ2 = 1
|
||||
__all__ = ['ZQZQ1']
|
||||
"#,
|
||||
)
|
||||
.completion_test_builder()
|
||||
.auto_import()
|
||||
.module_names()
|
||||
.build()
|
||||
.snapshot();
|
||||
// We specifically do not want `ZQZQ2` here, since
|
||||
// it is not part of `__all__`.
|
||||
assert_snapshot!(snapshot, @r"
|
||||
ZQZQ1 :: <no import required>
|
||||
ZQZQ2 :: <no import required>
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn auto_import_ignores_modules_with_leading_underscore() {
|
||||
let snapshot = CursorTest::builder()
|
||||
.source(
|
||||
"main.py",
|
||||
r#"
|
||||
Quitter<CURSOR>
|
||||
"#,
|
||||
)
|
||||
.completion_test_builder()
|
||||
.auto_import()
|
||||
.module_names()
|
||||
.build()
|
||||
.snapshot();
|
||||
// There is a `Quitter` in `_sitebuiltins` in the standard
|
||||
// library. But this is skipped by auto-import because it's
|
||||
// 1) not first party and 2) starts with an `_`.
|
||||
assert_snapshot!(snapshot, @"<No completions found>");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn auto_import_includes_modules_with_leading_underscore_in_first_party() {
|
||||
let snapshot = CursorTest::builder()
|
||||
.source(
|
||||
"main.py",
|
||||
r#"
|
||||
ZQ<CURSOR>
|
||||
"#,
|
||||
)
|
||||
.source(
|
||||
"bar.py",
|
||||
r#"
|
||||
ZQZQ1 = 1
|
||||
"#,
|
||||
)
|
||||
.source(
|
||||
"_foo.py",
|
||||
r#"
|
||||
ZQZQ1 = 1
|
||||
"#,
|
||||
)
|
||||
.completion_test_builder()
|
||||
.auto_import()
|
||||
.module_names()
|
||||
.build()
|
||||
.snapshot();
|
||||
assert_snapshot!(snapshot, @r"
|
||||
ZQZQ1 :: _foo
|
||||
ZQZQ1 :: bar
|
||||
");
|
||||
}
|
||||
|
||||
/// A way to create a simple single-file (named `main.py`) completion test
|
||||
/// builder.
|
||||
///
|
||||
@@ -6055,7 +6241,7 @@ from .imp<CURSOR>
|
||||
let module_name = c
|
||||
.module_name
|
||||
.map(ModuleName::as_str)
|
||||
.unwrap_or("Current module");
|
||||
.unwrap_or("<no import required>");
|
||||
snapshot = format!("{snapshot} :: {module_name}");
|
||||
}
|
||||
snapshot
|
||||
|
||||
@@ -1906,4 +1906,211 @@ func<CURSOR>_alias()
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn references_submodule_import_from_use() {
|
||||
let test = CursorTest::builder()
|
||||
.source(
|
||||
"mypackage/__init__.py",
|
||||
r#"
|
||||
from .subpkg.submod import val
|
||||
|
||||
x = sub<CURSOR>pkg
|
||||
"#,
|
||||
)
|
||||
.source("mypackage/subpkg/__init__.py", r#""#)
|
||||
.source(
|
||||
"mypackage/subpkg/submod.py",
|
||||
r#"
|
||||
val: int = 0
|
||||
"#,
|
||||
)
|
||||
.build();
|
||||
|
||||
// TODO(submodule-imports): this should light up both instances of `subpkg`
|
||||
assert_snapshot!(test.references(), @r"
|
||||
info[references]: Reference 1
|
||||
--> mypackage/__init__.py:4:5
|
||||
|
|
||||
2 | from .subpkg.submod import val
|
||||
3 |
|
||||
4 | x = subpkg
|
||||
| ^^^^^^
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn references_submodule_import_from_def() {
|
||||
let test = CursorTest::builder()
|
||||
.source(
|
||||
"mypackage/__init__.py",
|
||||
r#"
|
||||
from .sub<CURSOR>pkg.submod import val
|
||||
|
||||
x = subpkg
|
||||
"#,
|
||||
)
|
||||
.source("mypackage/subpkg/__init__.py", r#""#)
|
||||
.source(
|
||||
"mypackage/subpkg/submod.py",
|
||||
r#"
|
||||
val: int = 0
|
||||
"#,
|
||||
)
|
||||
.build();
|
||||
|
||||
// TODO(submodule-imports): this should light up both instances of `subpkg`
|
||||
assert_snapshot!(test.references(), @"No references found");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn references_submodule_import_from_wrong_use() {
|
||||
let test = CursorTest::builder()
|
||||
.source(
|
||||
"mypackage/__init__.py",
|
||||
r#"
|
||||
from .subpkg.submod import val
|
||||
|
||||
x = sub<CURSOR>mod
|
||||
"#,
|
||||
)
|
||||
.source("mypackage/subpkg/__init__.py", r#""#)
|
||||
.source(
|
||||
"mypackage/subpkg/submod.py",
|
||||
r#"
|
||||
val: int = 0
|
||||
"#,
|
||||
)
|
||||
.build();
|
||||
|
||||
// No references is actually correct (or it should only see itself)
|
||||
assert_snapshot!(test.references(), @"No references found");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn references_submodule_import_from_wrong_def() {
|
||||
let test = CursorTest::builder()
|
||||
.source(
|
||||
"mypackage/__init__.py",
|
||||
r#"
|
||||
from .subpkg.sub<CURSOR>mod import val
|
||||
|
||||
x = submod
|
||||
"#,
|
||||
)
|
||||
.source("mypackage/subpkg/__init__.py", r#""#)
|
||||
.source(
|
||||
"mypackage/subpkg/submod.py",
|
||||
r#"
|
||||
val: int = 0
|
||||
"#,
|
||||
)
|
||||
.build();
|
||||
|
||||
// No references is actually correct (or it should only see itself)
|
||||
assert_snapshot!(test.references(), @"No references found");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn references_submodule_import_from_confusing_shadowed_def() {
|
||||
let test = CursorTest::builder()
|
||||
.source(
|
||||
"mypackage/__init__.py",
|
||||
r#"
|
||||
from .sub<CURSOR>pkg import subpkg
|
||||
|
||||
x = subpkg
|
||||
"#,
|
||||
)
|
||||
.source(
|
||||
"mypackage/subpkg/__init__.py",
|
||||
r#"
|
||||
subpkg: int = 10
|
||||
"#,
|
||||
)
|
||||
.build();
|
||||
|
||||
// No references is actually correct (or it should only see itself)
|
||||
assert_snapshot!(test.references(), @"No references found");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn references_submodule_import_from_confusing_real_def() {
|
||||
let test = CursorTest::builder()
|
||||
.source(
|
||||
"mypackage/__init__.py",
|
||||
r#"
|
||||
from .subpkg import sub<CURSOR>pkg
|
||||
|
||||
x = subpkg
|
||||
"#,
|
||||
)
|
||||
.source(
|
||||
"mypackage/subpkg/__init__.py",
|
||||
r#"
|
||||
subpkg: int = 10
|
||||
"#,
|
||||
)
|
||||
.build();
|
||||
|
||||
assert_snapshot!(test.references(), @r"
|
||||
info[references]: Reference 1
|
||||
--> mypackage/__init__.py:2:21
|
||||
|
|
||||
2 | from .subpkg import subpkg
|
||||
| ^^^^^^
|
||||
3 |
|
||||
4 | x = subpkg
|
||||
|
|
||||
|
||||
info[references]: Reference 2
|
||||
--> mypackage/__init__.py:4:5
|
||||
|
|
||||
2 | from .subpkg import subpkg
|
||||
3 |
|
||||
4 | x = subpkg
|
||||
| ^^^^^^
|
||||
|
|
||||
|
||||
info[references]: Reference 3
|
||||
--> mypackage/subpkg/__init__.py:2:1
|
||||
|
|
||||
2 | subpkg: int = 10
|
||||
| ^^^^^^
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn references_submodule_import_from_confusing_use() {
|
||||
let test = CursorTest::builder()
|
||||
.source(
|
||||
"mypackage/__init__.py",
|
||||
r#"
|
||||
from .subpkg import subpkg
|
||||
|
||||
x = sub<CURSOR>pkg
|
||||
"#,
|
||||
)
|
||||
.source(
|
||||
"mypackage/subpkg/__init__.py",
|
||||
r#"
|
||||
subpkg: int = 10
|
||||
"#,
|
||||
)
|
||||
.build();
|
||||
|
||||
// TODO: this should also highlight the RHS subpkg in the import
|
||||
assert_snapshot!(test.references(), @r"
|
||||
info[references]: Reference 1
|
||||
--> mypackage/__init__.py:4:5
|
||||
|
|
||||
2 | from .subpkg import subpkg
|
||||
3 |
|
||||
4 | x = subpkg
|
||||
| ^^^^^^
|
||||
|
|
||||
");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2602,6 +2602,298 @@ def ab(a: int, *, c: int): ...
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_declaration_submodule_import_from_use() {
|
||||
let test = CursorTest::builder()
|
||||
.source(
|
||||
"mypackage/__init__.py",
|
||||
r#"
|
||||
from .subpkg.submod import val
|
||||
|
||||
x = sub<CURSOR>pkg
|
||||
"#,
|
||||
)
|
||||
.source("mypackage/subpkg/__init__.py", r#""#)
|
||||
.source(
|
||||
"mypackage/subpkg/submod.py",
|
||||
r#"
|
||||
val: int = 0
|
||||
"#,
|
||||
)
|
||||
.build();
|
||||
|
||||
// TODO(submodule-imports): this should only highlight `subpkg` in the import statement
|
||||
// This happens because DefinitionKind::ImportFromSubmodule claims the entire ImportFrom node,
|
||||
// which is correct but unhelpful. Unfortunately even if it only claimed the LHS identifier it
|
||||
// would highlight `subpkg.submod` which is strictly better but still isn't what we want.
|
||||
assert_snapshot!(test.goto_declaration(), @r"
|
||||
info[goto-declaration]: Declaration
|
||||
--> mypackage/__init__.py:2:1
|
||||
|
|
||||
2 | from .subpkg.submod import val
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
3 |
|
||||
4 | x = subpkg
|
||||
|
|
||||
info: Source
|
||||
--> mypackage/__init__.py:4:5
|
||||
|
|
||||
2 | from .subpkg.submod import val
|
||||
3 |
|
||||
4 | x = subpkg
|
||||
| ^^^^^^
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_declaration_submodule_import_from_def() {
|
||||
let test = CursorTest::builder()
|
||||
.source(
|
||||
"mypackage/__init__.py",
|
||||
r#"
|
||||
from .sub<CURSOR>pkg.submod import val
|
||||
|
||||
x = subpkg
|
||||
"#,
|
||||
)
|
||||
.source("mypackage/subpkg/__init__.py", r#""#)
|
||||
.source(
|
||||
"mypackage/subpkg/submod.py",
|
||||
r#"
|
||||
val: int = 0
|
||||
"#,
|
||||
)
|
||||
.build();
|
||||
|
||||
// TODO(submodule-imports): I don't *think* this is what we want..?
|
||||
// It's a bit confusing because this symbol is essentially the LHS *and* RHS of
|
||||
// `subpkg = mypackage.subpkg`. As in, it's both defining a local `subpkg` and
|
||||
// loading the module `mypackage.subpkg`, so, it's understandable to get confused!
|
||||
assert_snapshot!(test.goto_declaration(), @r"
|
||||
info[goto-declaration]: Declaration
|
||||
--> mypackage/subpkg/__init__.py:1:1
|
||||
|
|
||||
|
|
||||
info: Source
|
||||
--> mypackage/__init__.py:2:7
|
||||
|
|
||||
2 | from .subpkg.submod import val
|
||||
| ^^^^^^
|
||||
3 |
|
||||
4 | x = subpkg
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_declaration_submodule_import_from_wrong_use() {
|
||||
let test = CursorTest::builder()
|
||||
.source(
|
||||
"mypackage/__init__.py",
|
||||
r#"
|
||||
from .subpkg.submod import val
|
||||
|
||||
x = sub<CURSOR>mod
|
||||
"#,
|
||||
)
|
||||
.source("mypackage/subpkg/__init__.py", r#""#)
|
||||
.source(
|
||||
"mypackage/subpkg/submod.py",
|
||||
r#"
|
||||
val: int = 0
|
||||
"#,
|
||||
)
|
||||
.build();
|
||||
|
||||
// No result is correct!
|
||||
assert_snapshot!(test.goto_declaration(), @"No goto target found");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_declaration_submodule_import_from_wrong_def() {
|
||||
let test = CursorTest::builder()
|
||||
.source(
|
||||
"mypackage/__init__.py",
|
||||
r#"
|
||||
from .subpkg.sub<CURSOR>mod import val
|
||||
|
||||
x = submod
|
||||
"#,
|
||||
)
|
||||
.source("mypackage/subpkg/__init__.py", r#""#)
|
||||
.source(
|
||||
"mypackage/subpkg/submod.py",
|
||||
r#"
|
||||
val: int = 0
|
||||
"#,
|
||||
)
|
||||
.build();
|
||||
|
||||
// Going to the submod module is correct!
|
||||
assert_snapshot!(test.goto_declaration(), @r"
|
||||
info[goto-declaration]: Declaration
|
||||
--> mypackage/subpkg/submod.py:1:1
|
||||
|
|
||||
1 |
|
||||
| ^
|
||||
2 | val: int = 0
|
||||
|
|
||||
info: Source
|
||||
--> mypackage/__init__.py:2:14
|
||||
|
|
||||
2 | from .subpkg.submod import val
|
||||
| ^^^^^^
|
||||
3 |
|
||||
4 | x = submod
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_declaration_submodule_import_from_confusing_shadowed_def() {
|
||||
let test = CursorTest::builder()
|
||||
.source(
|
||||
"mypackage/__init__.py",
|
||||
r#"
|
||||
from .sub<CURSOR>pkg import subpkg
|
||||
|
||||
x = subpkg
|
||||
"#,
|
||||
)
|
||||
.source(
|
||||
"mypackage/subpkg/__init__.py",
|
||||
r#"
|
||||
subpkg: int = 10
|
||||
"#,
|
||||
)
|
||||
.build();
|
||||
|
||||
// Going to the subpkg module is correct!
|
||||
assert_snapshot!(test.goto_declaration(), @r"
|
||||
info[goto-declaration]: Declaration
|
||||
--> mypackage/subpkg/__init__.py:1:1
|
||||
|
|
||||
1 |
|
||||
| ^
|
||||
2 | subpkg: int = 10
|
||||
|
|
||||
info: Source
|
||||
--> mypackage/__init__.py:2:7
|
||||
|
|
||||
2 | from .subpkg import subpkg
|
||||
| ^^^^^^
|
||||
3 |
|
||||
4 | x = subpkg
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_declaration_submodule_import_from_confusing_real_def() {
|
||||
let test = CursorTest::builder()
|
||||
.source(
|
||||
"mypackage/__init__.py",
|
||||
r#"
|
||||
from .subpkg import sub<CURSOR>pkg
|
||||
|
||||
x = subpkg
|
||||
"#,
|
||||
)
|
||||
.source(
|
||||
"mypackage/subpkg/__init__.py",
|
||||
r#"
|
||||
subpkg: int = 10
|
||||
"#,
|
||||
)
|
||||
.build();
|
||||
|
||||
// Going to the subpkg `int` is correct!
|
||||
assert_snapshot!(test.goto_declaration(), @r"
|
||||
info[goto-declaration]: Declaration
|
||||
--> mypackage/subpkg/__init__.py:2:1
|
||||
|
|
||||
2 | subpkg: int = 10
|
||||
| ^^^^^^
|
||||
|
|
||||
info: Source
|
||||
--> mypackage/__init__.py:2:21
|
||||
|
|
||||
2 | from .subpkg import subpkg
|
||||
| ^^^^^^
|
||||
3 |
|
||||
4 | x = subpkg
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_declaration_submodule_import_from_confusing_use() {
|
||||
let test = CursorTest::builder()
|
||||
.source(
|
||||
"mypackage/__init__.py",
|
||||
r#"
|
||||
from .subpkg import subpkg
|
||||
|
||||
x = sub<CURSOR>pkg
|
||||
"#,
|
||||
)
|
||||
.source(
|
||||
"mypackage/subpkg/__init__.py",
|
||||
r#"
|
||||
subpkg: int = 10
|
||||
"#,
|
||||
)
|
||||
.build();
|
||||
|
||||
// TODO(submodule-imports): Ok this one is FASCINATING and it's kinda right but confusing!
|
||||
//
|
||||
// So there's 3 relevant definitions here:
|
||||
//
|
||||
// * `subpkg: int = 10` in the other file is in fact the original definition
|
||||
//
|
||||
// * the LHS `subpkg` in the import is an instance of `subpkg = ...`
|
||||
// because it's a `DefinitionKind::ImportFromSubmodle`.
|
||||
// This is the span that covers the entire import.
|
||||
//
|
||||
// * `the RHS `subpkg` in the import is a second instance of `subpkg = ...`
|
||||
// that *immediately* overwrites the `ImportFromSubmodule`'s definition
|
||||
// This span seemingly doesn't appear at all!? Is it getting hidden by the LHS span?
|
||||
assert_snapshot!(test.goto_declaration(), @r"
|
||||
info[goto-declaration]: Declaration
|
||||
--> mypackage/__init__.py:2:1
|
||||
|
|
||||
2 | from .subpkg import subpkg
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
3 |
|
||||
4 | x = subpkg
|
||||
|
|
||||
info: Source
|
||||
--> mypackage/__init__.py:4:5
|
||||
|
|
||||
2 | from .subpkg import subpkg
|
||||
3 |
|
||||
4 | x = subpkg
|
||||
| ^^^^^^
|
||||
|
|
||||
|
||||
info[goto-declaration]: Declaration
|
||||
--> mypackage/subpkg/__init__.py:2:1
|
||||
|
|
||||
2 | subpkg: int = 10
|
||||
| ^^^^^^
|
||||
|
|
||||
info: Source
|
||||
--> mypackage/__init__.py:4:5
|
||||
|
|
||||
2 | from .subpkg import subpkg
|
||||
3 |
|
||||
4 | x = subpkg
|
||||
| ^^^^^^
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
impl CursorTest {
|
||||
fn goto_declaration(&self) -> String {
|
||||
let Some(targets) = goto_declaration(&self.db, self.cursor.file, self.cursor.offset)
|
||||
|
||||
@@ -1672,6 +1672,283 @@ def function():
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_submodule_import_from_use() {
|
||||
let test = CursorTest::builder()
|
||||
.source(
|
||||
"mypackage/__init__.py",
|
||||
r#"
|
||||
from .subpkg.submod import val
|
||||
|
||||
x = sub<CURSOR>pkg
|
||||
"#,
|
||||
)
|
||||
.source("mypackage/subpkg/__init__.py", r#""#)
|
||||
.source(
|
||||
"mypackage/subpkg/submod.py",
|
||||
r#"
|
||||
val: int = 0
|
||||
"#,
|
||||
)
|
||||
.build();
|
||||
|
||||
// The module is the correct type definition
|
||||
assert_snapshot!(test.goto_type_definition(), @r"
|
||||
info[goto-type-definition]: Type definition
|
||||
--> mypackage/subpkg/__init__.py:1:1
|
||||
|
|
||||
|
|
||||
info: Source
|
||||
--> mypackage/__init__.py:4:5
|
||||
|
|
||||
2 | from .subpkg.submod import val
|
||||
3 |
|
||||
4 | x = subpkg
|
||||
| ^^^^^^
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_submodule_import_from_def() {
|
||||
let test = CursorTest::builder()
|
||||
.source(
|
||||
"mypackage/__init__.py",
|
||||
r#"
|
||||
from .sub<CURSOR>pkg.submod import val
|
||||
|
||||
x = subpkg
|
||||
"#,
|
||||
)
|
||||
.source("mypackage/subpkg/__init__.py", r#""#)
|
||||
.source(
|
||||
"mypackage/subpkg/submod.py",
|
||||
r#"
|
||||
val: int = 0
|
||||
"#,
|
||||
)
|
||||
.build();
|
||||
|
||||
// The module is the correct type definition
|
||||
assert_snapshot!(test.goto_type_definition(), @r"
|
||||
info[goto-type-definition]: Type definition
|
||||
--> mypackage/subpkg/__init__.py:1:1
|
||||
|
|
||||
|
|
||||
info: Source
|
||||
--> mypackage/__init__.py:2:7
|
||||
|
|
||||
2 | from .subpkg.submod import val
|
||||
| ^^^^^^
|
||||
3 |
|
||||
4 | x = subpkg
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_submodule_import_from_wrong_use() {
|
||||
let test = CursorTest::builder()
|
||||
.source(
|
||||
"mypackage/__init__.py",
|
||||
r#"
|
||||
from .subpkg.submod import val
|
||||
|
||||
x = sub<CURSOR>mod
|
||||
"#,
|
||||
)
|
||||
.source("mypackage/subpkg/__init__.py", r#""#)
|
||||
.source(
|
||||
"mypackage/subpkg/submod.py",
|
||||
r#"
|
||||
val: int = 0
|
||||
"#,
|
||||
)
|
||||
.build();
|
||||
|
||||
// Unknown is correct, `submod` is not in scope
|
||||
assert_snapshot!(test.goto_type_definition(), @r"
|
||||
info[goto-type-definition]: Type definition
|
||||
--> stdlib/ty_extensions.pyi:20:1
|
||||
|
|
||||
19 | # Types
|
||||
20 | Unknown = object()
|
||||
| ^^^^^^^
|
||||
21 | AlwaysTruthy = object()
|
||||
22 | AlwaysFalsy = object()
|
||||
|
|
||||
info: Source
|
||||
--> mypackage/__init__.py:4:5
|
||||
|
|
||||
2 | from .subpkg.submod import val
|
||||
3 |
|
||||
4 | x = submod
|
||||
| ^^^^^^
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_submodule_import_from_wrong_def() {
|
||||
let test = CursorTest::builder()
|
||||
.source(
|
||||
"mypackage/__init__.py",
|
||||
r#"
|
||||
from .subpkg.sub<CURSOR>mod import val
|
||||
|
||||
x = submod
|
||||
"#,
|
||||
)
|
||||
.source("mypackage/subpkg/__init__.py", r#""#)
|
||||
.source(
|
||||
"mypackage/subpkg/submod.py",
|
||||
r#"
|
||||
val: int = 0
|
||||
"#,
|
||||
)
|
||||
.build();
|
||||
|
||||
// The module is correct
|
||||
assert_snapshot!(test.goto_type_definition(), @r"
|
||||
info[goto-type-definition]: Type definition
|
||||
--> mypackage/subpkg/submod.py:1:1
|
||||
|
|
||||
1 | /
|
||||
2 | | val: int = 0
|
||||
| |_____________^
|
||||
|
|
||||
info: Source
|
||||
--> mypackage/__init__.py:2:14
|
||||
|
|
||||
2 | from .subpkg.submod import val
|
||||
| ^^^^^^
|
||||
3 |
|
||||
4 | x = submod
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_submodule_import_from_confusing_shadowed_def() {
|
||||
let test = CursorTest::builder()
|
||||
.source(
|
||||
"mypackage/__init__.py",
|
||||
r#"
|
||||
from .sub<CURSOR>pkg import subpkg
|
||||
|
||||
x = subpkg
|
||||
"#,
|
||||
)
|
||||
.source(
|
||||
"mypackage/subpkg/__init__.py",
|
||||
r#"
|
||||
subpkg: int = 10
|
||||
"#,
|
||||
)
|
||||
.build();
|
||||
|
||||
// The module is correct
|
||||
assert_snapshot!(test.goto_type_definition(), @r"
|
||||
info[goto-type-definition]: Type definition
|
||||
--> mypackage/subpkg/__init__.py:1:1
|
||||
|
|
||||
1 | /
|
||||
2 | | subpkg: int = 10
|
||||
| |_________________^
|
||||
|
|
||||
info: Source
|
||||
--> mypackage/__init__.py:2:7
|
||||
|
|
||||
2 | from .subpkg import subpkg
|
||||
| ^^^^^^
|
||||
3 |
|
||||
4 | x = subpkg
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_submodule_import_from_confusing_real_def() {
|
||||
let test = CursorTest::builder()
|
||||
.source(
|
||||
"mypackage/__init__.py",
|
||||
r#"
|
||||
from .subpkg import sub<CURSOR>pkg
|
||||
|
||||
x = subpkg
|
||||
"#,
|
||||
)
|
||||
.source(
|
||||
"mypackage/subpkg/__init__.py",
|
||||
r#"
|
||||
subpkg: int = 10
|
||||
"#,
|
||||
)
|
||||
.build();
|
||||
|
||||
// `int` is correct
|
||||
assert_snapshot!(test.goto_type_definition(), @r#"
|
||||
info[goto-type-definition]: Type definition
|
||||
--> stdlib/builtins.pyi:348:7
|
||||
|
|
||||
347 | @disjoint_base
|
||||
348 | class int:
|
||||
| ^^^
|
||||
349 | """int([x]) -> integer
|
||||
350 | int(x, base=10) -> integer
|
||||
|
|
||||
info: Source
|
||||
--> mypackage/__init__.py:2:21
|
||||
|
|
||||
2 | from .subpkg import subpkg
|
||||
| ^^^^^^
|
||||
3 |
|
||||
4 | x = subpkg
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_submodule_import_from_confusing_use() {
|
||||
let test = CursorTest::builder()
|
||||
.source(
|
||||
"mypackage/__init__.py",
|
||||
r#"
|
||||
from .subpkg import subpkg
|
||||
|
||||
x = sub<CURSOR>pkg
|
||||
"#,
|
||||
)
|
||||
.source(
|
||||
"mypackage/subpkg/__init__.py",
|
||||
r#"
|
||||
subpkg: int = 10
|
||||
"#,
|
||||
)
|
||||
.build();
|
||||
|
||||
// `int` is correct
|
||||
assert_snapshot!(test.goto_type_definition(), @r#"
|
||||
info[goto-type-definition]: Type definition
|
||||
--> stdlib/builtins.pyi:348:7
|
||||
|
|
||||
347 | @disjoint_base
|
||||
348 | class int:
|
||||
| ^^^
|
||||
349 | """int([x]) -> integer
|
||||
350 | int(x, base=10) -> integer
|
||||
|
|
||||
info: Source
|
||||
--> mypackage/__init__.py:4:5
|
||||
|
|
||||
2 | from .subpkg import subpkg
|
||||
3 |
|
||||
4 | x = subpkg
|
||||
| ^^^^^^
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
impl CursorTest {
|
||||
fn goto_type_definition(&self) -> String {
|
||||
let Some(targets) =
|
||||
|
||||
@@ -3321,6 +3321,297 @@ def function():
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hover_submodule_import_from_use() {
|
||||
let test = CursorTest::builder()
|
||||
.source(
|
||||
"mypackage/__init__.py",
|
||||
r#"
|
||||
from .subpkg.submod import val
|
||||
|
||||
x = sub<CURSOR>pkg
|
||||
"#,
|
||||
)
|
||||
.source("mypackage/subpkg/__init__.py", r#""#)
|
||||
.source(
|
||||
"mypackage/subpkg/submod.py",
|
||||
r#"
|
||||
val: int = 0
|
||||
"#,
|
||||
)
|
||||
.build();
|
||||
|
||||
// The module is correct
|
||||
assert_snapshot!(test.hover(), @r"
|
||||
<module 'mypackage.subpkg'>
|
||||
---------------------------------------------
|
||||
```python
|
||||
<module 'mypackage.subpkg'>
|
||||
```
|
||||
---------------------------------------------
|
||||
info[hover]: Hovered content is
|
||||
--> mypackage/__init__.py:4:5
|
||||
|
|
||||
2 | from .subpkg.submod import val
|
||||
3 |
|
||||
4 | x = subpkg
|
||||
| ^^^-^^
|
||||
| | |
|
||||
| | Cursor offset
|
||||
| source
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hover_submodule_import_from_def() {
|
||||
let test = CursorTest::builder()
|
||||
.source(
|
||||
"mypackage/__init__.py",
|
||||
r#"
|
||||
from .sub<CURSOR>pkg.submod import val
|
||||
|
||||
x = subpkg
|
||||
"#,
|
||||
)
|
||||
.source("mypackage/subpkg/__init__.py", r#""#)
|
||||
.source(
|
||||
"mypackage/subpkg/submod.py",
|
||||
r#"
|
||||
val: int = 0
|
||||
"#,
|
||||
)
|
||||
.build();
|
||||
|
||||
// The module is correct
|
||||
assert_snapshot!(test.hover(), @r"
|
||||
<module 'mypackage.subpkg'>
|
||||
---------------------------------------------
|
||||
```python
|
||||
<module 'mypackage.subpkg'>
|
||||
```
|
||||
---------------------------------------------
|
||||
info[hover]: Hovered content is
|
||||
--> mypackage/__init__.py:2:7
|
||||
|
|
||||
2 | from .subpkg.submod import val
|
||||
| ^^^-^^
|
||||
| | |
|
||||
| | Cursor offset
|
||||
| source
|
||||
3 |
|
||||
4 | x = subpkg
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hover_submodule_import_from_wrong_use() {
|
||||
let test = CursorTest::builder()
|
||||
.source(
|
||||
"mypackage/__init__.py",
|
||||
r#"
|
||||
from .subpkg.submod import val
|
||||
|
||||
x = sub<CURSOR>mod
|
||||
"#,
|
||||
)
|
||||
.source("mypackage/subpkg/__init__.py", r#""#)
|
||||
.source(
|
||||
"mypackage/subpkg/submod.py",
|
||||
r#"
|
||||
val: int = 0
|
||||
"#,
|
||||
)
|
||||
.build();
|
||||
|
||||
// Unknown is correct
|
||||
assert_snapshot!(test.hover(), @r"
|
||||
Unknown
|
||||
---------------------------------------------
|
||||
```python
|
||||
Unknown
|
||||
```
|
||||
---------------------------------------------
|
||||
info[hover]: Hovered content is
|
||||
--> mypackage/__init__.py:4:5
|
||||
|
|
||||
2 | from .subpkg.submod import val
|
||||
3 |
|
||||
4 | x = submod
|
||||
| ^^^-^^
|
||||
| | |
|
||||
| | Cursor offset
|
||||
| source
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hover_submodule_import_from_wrong_def() {
|
||||
let test = CursorTest::builder()
|
||||
.source(
|
||||
"mypackage/__init__.py",
|
||||
r#"
|
||||
from .subpkg.sub<CURSOR>mod import val
|
||||
|
||||
x = submod
|
||||
"#,
|
||||
)
|
||||
.source("mypackage/subpkg/__init__.py", r#""#)
|
||||
.source(
|
||||
"mypackage/subpkg/submod.py",
|
||||
r#"
|
||||
val: int = 0
|
||||
"#,
|
||||
)
|
||||
.build();
|
||||
|
||||
// The submodule is correct
|
||||
assert_snapshot!(test.hover(), @r"
|
||||
<module 'mypackage.subpkg.submod'>
|
||||
---------------------------------------------
|
||||
```python
|
||||
<module 'mypackage.subpkg.submod'>
|
||||
```
|
||||
---------------------------------------------
|
||||
info[hover]: Hovered content is
|
||||
--> mypackage/__init__.py:2:14
|
||||
|
|
||||
2 | from .subpkg.submod import val
|
||||
| ^^^-^^
|
||||
| | |
|
||||
| | Cursor offset
|
||||
| source
|
||||
3 |
|
||||
4 | x = submod
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hover_submodule_import_from_confusing_shadowed_def() {
|
||||
let test = CursorTest::builder()
|
||||
.source(
|
||||
"mypackage/__init__.py",
|
||||
r#"
|
||||
from .sub<CURSOR>pkg import subpkg
|
||||
|
||||
x = subpkg
|
||||
"#,
|
||||
)
|
||||
.source(
|
||||
"mypackage/subpkg/__init__.py",
|
||||
r#"
|
||||
subpkg: int = 10
|
||||
"#,
|
||||
)
|
||||
.build();
|
||||
|
||||
// The module is correct
|
||||
assert_snapshot!(test.hover(), @r"
|
||||
<module 'mypackage.subpkg'>
|
||||
---------------------------------------------
|
||||
```python
|
||||
<module 'mypackage.subpkg'>
|
||||
```
|
||||
---------------------------------------------
|
||||
info[hover]: Hovered content is
|
||||
--> mypackage/__init__.py:2:7
|
||||
|
|
||||
2 | from .subpkg import subpkg
|
||||
| ^^^-^^
|
||||
| | |
|
||||
| | Cursor offset
|
||||
| source
|
||||
3 |
|
||||
4 | x = subpkg
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hover_submodule_import_from_confusing_real_def() {
|
||||
let test = CursorTest::builder()
|
||||
.source(
|
||||
"mypackage/__init__.py",
|
||||
r#"
|
||||
from .subpkg import sub<CURSOR>pkg
|
||||
|
||||
x = subpkg
|
||||
"#,
|
||||
)
|
||||
.source(
|
||||
"mypackage/subpkg/__init__.py",
|
||||
r#"
|
||||
subpkg: int = 10
|
||||
"#,
|
||||
)
|
||||
.build();
|
||||
|
||||
// int is correct
|
||||
assert_snapshot!(test.hover(), @r"
|
||||
int
|
||||
---------------------------------------------
|
||||
```python
|
||||
int
|
||||
```
|
||||
---------------------------------------------
|
||||
info[hover]: Hovered content is
|
||||
--> mypackage/__init__.py:2:21
|
||||
|
|
||||
2 | from .subpkg import subpkg
|
||||
| ^^^-^^
|
||||
| | |
|
||||
| | Cursor offset
|
||||
| source
|
||||
3 |
|
||||
4 | x = subpkg
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hover_submodule_import_from_confusing_use() {
|
||||
let test = CursorTest::builder()
|
||||
.source(
|
||||
"mypackage/__init__.py",
|
||||
r#"
|
||||
from .subpkg import subpkg
|
||||
|
||||
x = sub<CURSOR>pkg
|
||||
"#,
|
||||
)
|
||||
.source(
|
||||
"mypackage/subpkg/__init__.py",
|
||||
r#"
|
||||
subpkg: int = 10
|
||||
"#,
|
||||
)
|
||||
.build();
|
||||
|
||||
// int is correct
|
||||
assert_snapshot!(test.hover(), @r"
|
||||
int
|
||||
---------------------------------------------
|
||||
```python
|
||||
int
|
||||
```
|
||||
---------------------------------------------
|
||||
info[hover]: Hovered content is
|
||||
--> mypackage/__init__.py:4:5
|
||||
|
|
||||
2 | from .subpkg import subpkg
|
||||
3 |
|
||||
4 | x = subpkg
|
||||
| ^^^-^^
|
||||
| | |
|
||||
| | Cursor offset
|
||||
| source
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
impl CursorTest {
|
||||
fn hover(&self) -> String {
|
||||
use std::fmt::Write;
|
||||
|
||||
@@ -1223,4 +1223,207 @@ result = func(10, y=20)
|
||||
|
||||
assert_snapshot!(test.rename("z"), @"Cannot rename");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rename_submodule_import_from_use() {
|
||||
let test = CursorTest::builder()
|
||||
.source(
|
||||
"mypackage/__init__.py",
|
||||
r#"
|
||||
from .subpkg.submod import val
|
||||
|
||||
x = sub<CURSOR>pkg
|
||||
"#,
|
||||
)
|
||||
.source("mypackage/subpkg/__init__.py", r#""#)
|
||||
.source(
|
||||
"mypackage/subpkg/submod.py",
|
||||
r#"
|
||||
val: int = 0
|
||||
"#,
|
||||
)
|
||||
.build();
|
||||
|
||||
// TODO(submodule-imports): we should refuse to rename this (it's the name of a module)
|
||||
assert_snapshot!(test.rename("mypkg"), @r"
|
||||
info[rename]: Rename symbol (found 1 locations)
|
||||
--> mypackage/__init__.py:4:5
|
||||
|
|
||||
2 | from .subpkg.submod import val
|
||||
3 |
|
||||
4 | x = subpkg
|
||||
| ^^^^^^
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rename_submodule_import_from_def() {
|
||||
let test = CursorTest::builder()
|
||||
.source(
|
||||
"mypackage/__init__.py",
|
||||
r#"
|
||||
from .sub<CURSOR>pkg.submod import val
|
||||
|
||||
x = subpkg
|
||||
"#,
|
||||
)
|
||||
.source("mypackage/subpkg/__init__.py", r#""#)
|
||||
.source(
|
||||
"mypackage/subpkg/submod.py",
|
||||
r#"
|
||||
val: int = 0
|
||||
"#,
|
||||
)
|
||||
.build();
|
||||
|
||||
// Refusing to rename is correct
|
||||
assert_snapshot!(test.rename("mypkg"), @"Cannot rename");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rename_submodule_import_from_wrong_use() {
|
||||
let test = CursorTest::builder()
|
||||
.source(
|
||||
"mypackage/__init__.py",
|
||||
r#"
|
||||
from .subpkg.submod import val
|
||||
|
||||
x = sub<CURSOR>mod
|
||||
"#,
|
||||
)
|
||||
.source("mypackage/subpkg/__init__.py", r#""#)
|
||||
.source(
|
||||
"mypackage/subpkg/submod.py",
|
||||
r#"
|
||||
val: int = 0
|
||||
"#,
|
||||
)
|
||||
.build();
|
||||
|
||||
// Refusing to rename is good/fine here, it's an undefined reference
|
||||
assert_snapshot!(test.rename("mypkg"), @"Cannot rename");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rename_submodule_import_from_wrong_def() {
|
||||
let test = CursorTest::builder()
|
||||
.source(
|
||||
"mypackage/__init__.py",
|
||||
r#"
|
||||
from .subpkg.sub<CURSOR>mod import val
|
||||
|
||||
x = submod
|
||||
"#,
|
||||
)
|
||||
.source("mypackage/subpkg/__init__.py", r#""#)
|
||||
.source(
|
||||
"mypackage/subpkg/submod.py",
|
||||
r#"
|
||||
val: int = 0
|
||||
"#,
|
||||
)
|
||||
.build();
|
||||
|
||||
// Refusing to rename is good here, it's a module name
|
||||
assert_snapshot!(test.rename("mypkg"), @"Cannot rename");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rename_submodule_import_from_confusing_shadowed_def() {
|
||||
let test = CursorTest::builder()
|
||||
.source(
|
||||
"mypackage/__init__.py",
|
||||
r#"
|
||||
from .sub<CURSOR>pkg import subpkg
|
||||
|
||||
x = subpkg
|
||||
"#,
|
||||
)
|
||||
.source(
|
||||
"mypackage/subpkg/__init__.py",
|
||||
r#"
|
||||
subpkg: int = 10
|
||||
"#,
|
||||
)
|
||||
.build();
|
||||
|
||||
// Refusing to rename is good here, it's the name of a module
|
||||
assert_snapshot!(test.rename("mypkg"), @"Cannot rename");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rename_submodule_import_from_confusing_real_def() {
|
||||
let test = CursorTest::builder()
|
||||
.source(
|
||||
"mypackage/__init__.py",
|
||||
r#"
|
||||
from .subpkg import sub<CURSOR>pkg
|
||||
|
||||
x = subpkg
|
||||
"#,
|
||||
)
|
||||
.source(
|
||||
"mypackage/subpkg/__init__.py",
|
||||
r#"
|
||||
subpkg: int = 10
|
||||
"#,
|
||||
)
|
||||
.build();
|
||||
|
||||
// Renaming the integer is correct
|
||||
assert_snapshot!(test.rename("mypkg"), @r"
|
||||
info[rename]: Rename symbol (found 3 locations)
|
||||
--> mypackage/__init__.py:2:21
|
||||
|
|
||||
2 | from .subpkg import subpkg
|
||||
| ^^^^^^
|
||||
3 |
|
||||
4 | x = subpkg
|
||||
| ------
|
||||
|
|
||||
::: mypackage/subpkg/__init__.py:2:1
|
||||
|
|
||||
2 | subpkg: int = 10
|
||||
| ------
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rename_submodule_import_from_confusing_use() {
|
||||
let test = CursorTest::builder()
|
||||
.source(
|
||||
"mypackage/__init__.py",
|
||||
r#"
|
||||
from .subpkg import subpkg
|
||||
|
||||
x = sub<CURSOR>pkg
|
||||
"#,
|
||||
)
|
||||
.source(
|
||||
"mypackage/subpkg/__init__.py",
|
||||
r#"
|
||||
subpkg: int = 10
|
||||
"#,
|
||||
)
|
||||
.build();
|
||||
|
||||
// TODO(submodule-imports): this is incorrect, we should rename the `subpkg` int
|
||||
// and the RHS of the import statement (but *not* rename the LHS).
|
||||
//
|
||||
// However us being cautious here *would* be good as the rename will actually
|
||||
// result in a `subpkg` variable still existing in this code, as the import's LHS
|
||||
// `DefinitionKind::ImportFromSubmodule` would stop being overwritten by the RHS!
|
||||
assert_snapshot!(test.rename("mypkg"), @r"
|
||||
info[rename]: Rename symbol (found 1 locations)
|
||||
--> mypackage/__init__.py:4:5
|
||||
|
|
||||
2 | from .subpkg import subpkg
|
||||
3 |
|
||||
4 | x = subpkg
|
||||
| ^^^^^^
|
||||
|
|
||||
");
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -594,7 +594,7 @@ impl SearchPath {
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn is_first_party(&self) -> bool {
|
||||
pub fn is_first_party(&self) -> bool {
|
||||
matches!(&*self.0, SearchPathInner::FirstParty(_))
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@ use ty_server::ClientOptions;
|
||||
|
||||
use crate::{TestServer, TestServerBuilder};
|
||||
|
||||
static FILTERS: &[(&str, &str)] = &[(r#""sortText": "[0-9 ]+""#, r#""sortText": "[RANKING]""#)];
|
||||
|
||||
#[test]
|
||||
fn publish_diagnostics_open() -> anyhow::Result<()> {
|
||||
let mut server = TestServerBuilder::new()?
|
||||
@@ -309,7 +311,11 @@ b: Litera
|
||||
|
||||
let completions = literal_completions(&mut server, &second_cell, Position::new(1, 9));
|
||||
|
||||
assert_json_snapshot!(completions);
|
||||
insta::with_settings!({
|
||||
filters => FILTERS.iter().copied(),
|
||||
}, {
|
||||
assert_json_snapshot!(completions);
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -340,7 +346,11 @@ b: Litera
|
||||
|
||||
let completions = literal_completions(&mut server, &first_cell, Position::new(1, 9));
|
||||
|
||||
assert_json_snapshot!(completions);
|
||||
insta::with_settings!({
|
||||
filters => FILTERS.iter().copied(),
|
||||
}, {
|
||||
assert_json_snapshot!(completions);
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -373,7 +383,11 @@ b: Litera
|
||||
|
||||
let completions = literal_completions(&mut server, &second_cell, Position::new(1, 9));
|
||||
|
||||
assert_json_snapshot!(completions);
|
||||
insta::with_settings!({
|
||||
filters => FILTERS.iter().copied(),
|
||||
}, {
|
||||
assert_json_snapshot!(completions);
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -409,7 +423,11 @@ b: Litera
|
||||
|
||||
let completions = literal_completions(&mut server, &second_cell, Position::new(1, 9));
|
||||
|
||||
assert_json_snapshot!(completions);
|
||||
insta::with_settings!({
|
||||
filters => FILTERS.iter().copied(),
|
||||
}, {
|
||||
assert_json_snapshot!(completions);
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ expression: completions
|
||||
{
|
||||
"label": "Literal (import typing)",
|
||||
"kind": 6,
|
||||
"sortText": " 50",
|
||||
"sortText": "[RANKING]",
|
||||
"insertText": "Literal",
|
||||
"additionalTextEdits": [
|
||||
{
|
||||
@@ -27,7 +27,7 @@ expression: completions
|
||||
{
|
||||
"label": "LiteralString (import typing)",
|
||||
"kind": 6,
|
||||
"sortText": " 51",
|
||||
"sortText": "[RANKING]",
|
||||
"insertText": "LiteralString",
|
||||
"additionalTextEdits": [
|
||||
{
|
||||
|
||||
@@ -6,7 +6,7 @@ expression: completions
|
||||
{
|
||||
"label": "Literal (import typing)",
|
||||
"kind": 6,
|
||||
"sortText": " 50",
|
||||
"sortText": "[RANKING]",
|
||||
"insertText": "Literal",
|
||||
"additionalTextEdits": [
|
||||
{
|
||||
@@ -27,7 +27,7 @@ expression: completions
|
||||
{
|
||||
"label": "LiteralString (import typing)",
|
||||
"kind": 6,
|
||||
"sortText": " 51",
|
||||
"sortText": "[RANKING]",
|
||||
"insertText": "LiteralString",
|
||||
"additionalTextEdits": [
|
||||
{
|
||||
|
||||
@@ -6,7 +6,7 @@ expression: completions
|
||||
{
|
||||
"label": "Literal (import typing)",
|
||||
"kind": 6,
|
||||
"sortText": " 50",
|
||||
"sortText": "[RANKING]",
|
||||
"insertText": "Literal",
|
||||
"additionalTextEdits": [
|
||||
{
|
||||
@@ -27,7 +27,7 @@ expression: completions
|
||||
{
|
||||
"label": "LiteralString (import typing)",
|
||||
"kind": 6,
|
||||
"sortText": " 51",
|
||||
"sortText": "[RANKING]",
|
||||
"insertText": "LiteralString",
|
||||
"additionalTextEdits": [
|
||||
{
|
||||
|
||||
@@ -6,7 +6,7 @@ expression: completions
|
||||
{
|
||||
"label": "Literal (import typing)",
|
||||
"kind": 6,
|
||||
"sortText": " 50",
|
||||
"sortText": "[RANKING]",
|
||||
"insertText": "Literal",
|
||||
"additionalTextEdits": [
|
||||
{
|
||||
@@ -27,7 +27,7 @@ expression: completions
|
||||
{
|
||||
"label": "LiteralString (import typing)",
|
||||
"kind": 6,
|
||||
"sortText": " 51",
|
||||
"sortText": "[RANKING]",
|
||||
"insertText": "LiteralString",
|
||||
"additionalTextEdits": [
|
||||
{
|
||||
|
||||
21
playground/package-lock.json
generated
21
playground/package-lock.json
generated
@@ -9,6 +9,7 @@
|
||||
"version": "0.0.0",
|
||||
"workspaces": [
|
||||
"ty",
|
||||
"ty-embed",
|
||||
"ruff",
|
||||
"shared"
|
||||
],
|
||||
@@ -6081,6 +6082,10 @@
|
||||
"resolved": "ty/ty_wasm",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/ty-embed": {
|
||||
"resolved": "ty-embed",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/ty-playground": {
|
||||
"resolved": "ty",
|
||||
"link": true
|
||||
@@ -6608,6 +6613,22 @@
|
||||
"vite-plugin-static-copy": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"ty-embed": {
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"monaco-editor": "^0.54.0",
|
||||
"ty_wasm": "file:ty_wasm"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.8.2",
|
||||
"vite": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"ty-embed/ty_wasm": {
|
||||
"version": "0.0.0",
|
||||
"extraneous": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"ty/ty_wasm": {
|
||||
"version": "0.0.0",
|
||||
"license": "MIT"
|
||||
|
||||
@@ -9,11 +9,12 @@
|
||||
"dev:build": "npm run dev:build --workspace ty-playground && npm run dev:build --workspace ruff-playground",
|
||||
"fmt": "prettier --cache -w .",
|
||||
"fmt:check": "prettier --cache --check .",
|
||||
"lint": "eslint --cache --ext .ts,.tsx ruff/src ty/src",
|
||||
"lint": "eslint --cache --ext .ts,.tsx ruff/src ty/src ty-embed/src",
|
||||
"tsc": "tsc"
|
||||
},
|
||||
"workspaces": [
|
||||
"ty",
|
||||
"ty-embed",
|
||||
"ruff",
|
||||
"shared"
|
||||
],
|
||||
|
||||
5
playground/ty-embed/.gitignore
vendored
Normal file
5
playground/ty-embed/.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
node_modules
|
||||
dist
|
||||
ty_wasm
|
||||
.DS_Store
|
||||
*.log
|
||||
3
playground/ty-embed/README.md
Normal file
3
playground/ty-embed/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# ty Embeddable Editor
|
||||
|
||||
A simplified, embeddable version of the ty playground that can be used in documentation pages. This allows you to create multiple interactive Python type-checking editors on a single webpage.
|
||||
91
playground/ty-embed/example-dev.html
Normal file
91
playground/ty-embed/example-dev.html
Normal file
@@ -0,0 +1,91 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>ty Type Checker - Interactive Example</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
||||
sans-serif;
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
padding: 40px 20px;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #1a1a1a;
|
||||
padding-bottom: 12px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: #2a2a2a;
|
||||
margin-top: 30px;
|
||||
margin-bottom: 16px;
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
p {
|
||||
color: #555;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.intro {
|
||||
font-size: 1.1em;
|
||||
color: #666;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.editor-container {
|
||||
margin: 24px 0 48px 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>
|
||||
ty Type Checker
|
||||
</h1>
|
||||
<p class="intro">
|
||||
This interactive editor provides real-time type checking.
|
||||
Hover over symbols to see type information, Ctrl+click to jump to definitions,
|
||||
and click on errors to see quick fix suggestions.
|
||||
</p>
|
||||
|
||||
<h2>TypedDict Keys</h2>
|
||||
<p>
|
||||
The code below has a typo in the dictionary key. Click on the error
|
||||
and use the quick fix (lightbulb icon or Ctrl+.) to automatically fix it:
|
||||
</p>
|
||||
<div id="editor" class="editor-container"></div>
|
||||
|
||||
<script type="module">
|
||||
// Development version - imports from source
|
||||
import { createTyEditor } from "./src/index.ts";
|
||||
|
||||
const editor = createTyEditor({
|
||||
container: "#editor",
|
||||
id: "typeddict-example",
|
||||
theme: "light",
|
||||
height: "360px",
|
||||
initialCode: `from typing import TypedDict
|
||||
|
||||
class Person(TypedDict):
|
||||
name: str
|
||||
age: int | None
|
||||
|
||||
def greet(person: Person):
|
||||
print("Hello", person["name"])
|
||||
`,
|
||||
});
|
||||
|
||||
// Expose for debugging
|
||||
window.tyEditor = editor;
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
89
playground/ty-embed/example.html
Normal file
89
playground/ty-embed/example.html
Normal file
@@ -0,0 +1,89 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>ty Type Checker - Interactive Example</title>
|
||||
<link rel="stylesheet" href="./dist/ty-embed.css" />
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
||||
sans-serif;
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
padding: 40px 20px;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #1a1a1a;
|
||||
padding-bottom: 12px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: #2a2a2a;
|
||||
margin-top: 30px;
|
||||
margin-bottom: 16px;
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
p {
|
||||
color: #555;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.intro {
|
||||
font-size: 1.1em;
|
||||
color: #666;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.editor-container {
|
||||
margin: 24px 0 48px 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>
|
||||
ty Type Checker
|
||||
</h1>
|
||||
<p class="intro">
|
||||
This interactive editor provides real-time type checking.
|
||||
Hover over symbols to see type information, Ctrl+click to jump to definitions,
|
||||
and click on errors to see quick fix suggestions.
|
||||
</p>
|
||||
|
||||
<h2>TypedDict Keys</h2>
|
||||
<p>
|
||||
The code below has a typo in the dictionary key. Click on the error
|
||||
and use the quick fix (lightbulb icon or Ctrl+.) to automatically fix it:
|
||||
</p>
|
||||
<div id="editor" class="editor-container"></div>
|
||||
|
||||
<!-- Load the ty-embed library -->
|
||||
<script type="module">
|
||||
import { createTyEditor } from "./dist/ty-embed.es.js";
|
||||
|
||||
createTyEditor({
|
||||
container: "#editor",
|
||||
id: "typeddict-example",
|
||||
theme: "light",
|
||||
height: "360px",
|
||||
initialCode: `from typing import TypedDict
|
||||
|
||||
class Person(TypedDict):
|
||||
name: str
|
||||
age: int | None
|
||||
|
||||
def greet(person: Person):
|
||||
print("Hello", person["name"])
|
||||
`,
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
25
playground/ty-embed/package.json
Normal file
25
playground/ty-embed/package.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "ty-embed",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"prebuild": "npm run build:wasm",
|
||||
"build": "vite build",
|
||||
"build:wasm": "wasm-pack build ../../crates/ty_wasm --target web --out-dir ../../playground/ty-embed/ty_wasm",
|
||||
"dev:wasm": "wasm-pack build ../../crates/ty_wasm --dev --target web --out-dir ../../playground/ty-embed/ty_wasm",
|
||||
"predev:build": "npm run dev:wasm",
|
||||
"dev:build": "vite build",
|
||||
"prestart": "npm run dev:wasm",
|
||||
"start": "vite"
|
||||
},
|
||||
"dependencies": {
|
||||
"monaco-editor": "^0.54.0",
|
||||
"ty_wasm": "file:ty_wasm"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.8.2",
|
||||
"vite": "^7.0.0",
|
||||
"vite-plugin-static-copy": "^2.2.0"
|
||||
}
|
||||
}
|
||||
843
playground/ty-embed/src/EmbeddableEditor.ts
Normal file
843
playground/ty-embed/src/EmbeddableEditor.ts
Normal file
@@ -0,0 +1,843 @@
|
||||
import * as monaco from "monaco-editor";
|
||||
import {
|
||||
FileHandle,
|
||||
PositionEncoding,
|
||||
Workspace,
|
||||
Range as TyRange,
|
||||
Severity,
|
||||
Position as TyPosition,
|
||||
CompletionKind,
|
||||
LocationLink,
|
||||
TextEdit,
|
||||
InlayHintKind,
|
||||
Diagnostic as TyDiagnostic,
|
||||
} from "ty_wasm";
|
||||
|
||||
// Ayu theme colors from the ty playground
|
||||
const RADIATE = "#d7ff64";
|
||||
const ROCK = "#78876e";
|
||||
const COSMIC = "#de5fe9";
|
||||
const SUN = "#ffac2f";
|
||||
const ELECTRON = "#46ebe1";
|
||||
const CONSTELLATION = "#5f6de9";
|
||||
const STARLIGHT = "#f4f4f1";
|
||||
const PROTON = "#f6afbc";
|
||||
const SUPERNOVA = "#f1aff6";
|
||||
const ASTEROID = "#e3cee3";
|
||||
|
||||
let themesInitialized = false;
|
||||
|
||||
function defineAyuThemes() {
|
||||
if (themesInitialized) return;
|
||||
themesInitialized = true;
|
||||
|
||||
// Ayu Light theme
|
||||
monaco.editor.defineTheme("Ayu-Light", {
|
||||
inherit: false,
|
||||
base: "vs",
|
||||
colors: {
|
||||
"editor.background": "#f8f9fa",
|
||||
"editor.foreground": "#5c6166",
|
||||
"editorLineNumber.foreground": "#8a919966",
|
||||
"editorLineNumber.activeForeground": "#8a9199cc",
|
||||
"editorCursor.foreground": "#ffaa33",
|
||||
"editor.selectionBackground": "#035bd626",
|
||||
"editor.lineHighlightBackground": "#8a91991a",
|
||||
"editorIndentGuide.background": "#8a91992e",
|
||||
"editorIndentGuide.activeBackground": "#8a919959",
|
||||
"editorError.foreground": "#e65050",
|
||||
"editorWarning.foreground": "#ffaa33",
|
||||
"editorWidget.background": "#f3f4f5",
|
||||
"editorWidget.border": "#6b7d8f1f",
|
||||
"editorHoverWidget.background": "#f3f4f5",
|
||||
"editorHoverWidget.border": "#6b7d8f1f",
|
||||
"editorSuggestWidget.background": "#f3f4f5",
|
||||
"editorSuggestWidget.border": "#6b7d8f1f",
|
||||
"editorSuggestWidget.highlightForeground": "#ffaa33",
|
||||
"editorSuggestWidget.selectedBackground": "#56728f1f",
|
||||
},
|
||||
rules: [
|
||||
{ fontStyle: "italic", foreground: "#787b8099", token: "comment" },
|
||||
{ foreground: COSMIC, token: "keyword" },
|
||||
{ foreground: COSMIC, token: "builtinConstant" },
|
||||
{ foreground: CONSTELLATION, token: "number" },
|
||||
{ foreground: ROCK, token: "tag" },
|
||||
{ foreground: ROCK, token: "string" },
|
||||
{ foreground: SUN, token: "method" },
|
||||
{ foreground: SUN, token: "function" },
|
||||
{ foreground: SUN, token: "decorator" },
|
||||
],
|
||||
encodedTokensColors: [],
|
||||
});
|
||||
|
||||
// Ayu Dark theme
|
||||
monaco.editor.defineTheme("Ayu-Dark", {
|
||||
inherit: false,
|
||||
base: "vs-dark",
|
||||
colors: {
|
||||
"editor.background": "#0b0e14",
|
||||
"editor.foreground": "#bfbdb6",
|
||||
"editorLineNumber.foreground": "#6c738099",
|
||||
"editorLineNumber.activeForeground": "#6c7380e6",
|
||||
"editorCursor.foreground": "#e6b450",
|
||||
"editor.selectionBackground": "#409fff4d",
|
||||
"editor.lineHighlightBackground": "#131721",
|
||||
"editorIndentGuide.background": "#6c738033",
|
||||
"editorIndentGuide.activeBackground": "#6c738080",
|
||||
"editorError.foreground": "#d95757",
|
||||
"editorWarning.foreground": "#e6b450",
|
||||
"editorWidget.background": "#0f131a",
|
||||
"editorWidget.border": "#11151c",
|
||||
"editorHoverWidget.background": "#0f131a",
|
||||
"editorHoverWidget.border": "#11151c",
|
||||
"editorSuggestWidget.background": "#0f131a",
|
||||
"editorSuggestWidget.border": "#11151c",
|
||||
"editorSuggestWidget.highlightForeground": "#e6b450",
|
||||
"editorSuggestWidget.selectedBackground": "#47526640",
|
||||
},
|
||||
rules: [
|
||||
{ fontStyle: "italic", foreground: "#acb6bf8c", token: "comment" },
|
||||
{ foreground: ELECTRON, token: "string" },
|
||||
{ foreground: CONSTELLATION, token: "number" },
|
||||
{ foreground: STARLIGHT, token: "identifier" },
|
||||
{ foreground: RADIATE, token: "keyword" },
|
||||
{ foreground: RADIATE, token: "builtinConstant" },
|
||||
{ foreground: PROTON, token: "tag" },
|
||||
{ foreground: ASTEROID, token: "delimiter" },
|
||||
{ foreground: SUPERNOVA, token: "class" },
|
||||
{ foreground: STARLIGHT, token: "variable" },
|
||||
{ foreground: STARLIGHT, token: "parameter" },
|
||||
{ foreground: SUN, token: "method" },
|
||||
{ foreground: SUN, token: "function" },
|
||||
{ foreground: SUN, token: "decorator" },
|
||||
],
|
||||
encodedTokensColors: [],
|
||||
});
|
||||
}
|
||||
|
||||
export interface EditorOptions {
|
||||
initialCode?: string;
|
||||
theme?: "light" | "dark";
|
||||
fileName?: string;
|
||||
settings?: Record<string, any>;
|
||||
height?: string;
|
||||
showDiagnostics?: boolean;
|
||||
id?: string;
|
||||
}
|
||||
|
||||
interface Diagnostic {
|
||||
id: string;
|
||||
message: string;
|
||||
severity: Severity;
|
||||
range: TyRange | null;
|
||||
raw: TyDiagnostic;
|
||||
}
|
||||
|
||||
const DEFAULT_SETTINGS = {
|
||||
environment: {
|
||||
"python-version": "3.14",
|
||||
},
|
||||
rules: {
|
||||
"undefined-reveal": "ignore",
|
||||
},
|
||||
};
|
||||
|
||||
export class EmbeddableEditor {
|
||||
private container: HTMLElement;
|
||||
private options: Required<EditorOptions>;
|
||||
private editor: monaco.editor.IStandaloneCodeEditor | null = null;
|
||||
private workspace: Workspace | null = null;
|
||||
private fileHandle: FileHandle | null = null;
|
||||
private languageServer: LanguageServer | null = null;
|
||||
private diagnosticsContainer: HTMLElement | null = null;
|
||||
private editorContainer: HTMLElement | null = null;
|
||||
private errorContainer: HTMLElement | null = null;
|
||||
private checkTimeoutId: number | null = null;
|
||||
|
||||
constructor(container: HTMLElement | string, options: EditorOptions) {
|
||||
const element =
|
||||
typeof container === "string"
|
||||
? document.querySelector(container)
|
||||
: container;
|
||||
|
||||
if (!element) {
|
||||
throw new Error(`Container not found: ${container}`);
|
||||
}
|
||||
|
||||
this.container = element as HTMLElement;
|
||||
this.options = {
|
||||
initialCode: options.initialCode ?? "",
|
||||
theme: options.theme ?? "light",
|
||||
fileName: options.fileName ?? "main.py",
|
||||
settings: options.settings ?? DEFAULT_SETTINGS,
|
||||
height: options.height ?? "400px",
|
||||
showDiagnostics: options.showDiagnostics ?? true,
|
||||
id: options.id ?? `editor-${Date.now()}`,
|
||||
};
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
private async init() {
|
||||
try {
|
||||
// Create container structure
|
||||
this.createContainerStructure();
|
||||
|
||||
// Initialize ty workspace
|
||||
const ty = await import("ty_wasm");
|
||||
await ty.default();
|
||||
|
||||
this.workspace = new Workspace("/", PositionEncoding.Utf16, {});
|
||||
this.workspace.updateOptions(this.options.settings);
|
||||
this.fileHandle = this.workspace.openFile(
|
||||
this.options.fileName,
|
||||
this.options.initialCode,
|
||||
);
|
||||
|
||||
// Initialize Monaco editor
|
||||
if (this.editorContainer) {
|
||||
// Define Ayu themes before creating the editor
|
||||
defineAyuThemes();
|
||||
|
||||
// Create model with URI matching the workspace file path
|
||||
const modelUri = monaco.Uri.parse(this.options.fileName);
|
||||
const model = monaco.editor.createModel(
|
||||
this.options.initialCode,
|
||||
"python",
|
||||
modelUri,
|
||||
);
|
||||
|
||||
this.editor = monaco.editor.create(this.editorContainer, {
|
||||
model: model,
|
||||
theme: this.options.theme === "light" ? "Ayu-Light" : "Ayu-Dark",
|
||||
minimap: { enabled: false },
|
||||
fontSize: 14,
|
||||
scrollBeyondLastLine: false,
|
||||
roundedSelection: false,
|
||||
contextmenu: true,
|
||||
automaticLayout: true,
|
||||
fixedOverflowWidgets: true,
|
||||
});
|
||||
|
||||
// Setup language server features
|
||||
this.languageServer = new LanguageServer(
|
||||
this.workspace,
|
||||
this.fileHandle,
|
||||
);
|
||||
|
||||
// Listen to content changes
|
||||
this.editor.onDidChangeModelContent(() => {
|
||||
this.onContentChange();
|
||||
});
|
||||
|
||||
// Initial check
|
||||
this.checkFile();
|
||||
}
|
||||
} catch (err) {
|
||||
this.showError(this.formatError(err));
|
||||
}
|
||||
}
|
||||
|
||||
private createContainerStructure() {
|
||||
const isLight = this.options.theme === "light";
|
||||
|
||||
this.container.style.height = this.options.height;
|
||||
this.container.style.display = "flex";
|
||||
this.container.style.flexDirection = "column";
|
||||
this.container.style.border = isLight
|
||||
? "1px solid #6b7d8f1f"
|
||||
: "1px solid #11151c";
|
||||
this.container.style.borderRadius = "4px";
|
||||
this.container.style.overflow = "hidden";
|
||||
this.container.style.fontFamily =
|
||||
'Roboto Mono, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace';
|
||||
|
||||
// Editor container
|
||||
this.editorContainer = document.createElement("div");
|
||||
this.editorContainer.style.height = this.options.showDiagnostics
|
||||
? `calc(${this.options.height} - 120px)`
|
||||
: this.options.height;
|
||||
this.container.appendChild(this.editorContainer);
|
||||
|
||||
// Diagnostics container
|
||||
if (this.options.showDiagnostics) {
|
||||
this.diagnosticsContainer = document.createElement("div");
|
||||
this.diagnosticsContainer.style.height = "120px";
|
||||
this.diagnosticsContainer.style.overflow = "auto";
|
||||
this.diagnosticsContainer.style.borderTop = isLight
|
||||
? "1px solid #6b7d8f1f"
|
||||
: "1px solid #11151c";
|
||||
this.diagnosticsContainer.style.backgroundColor = isLight
|
||||
? "#f8f9fa"
|
||||
: "#0b0e14";
|
||||
this.diagnosticsContainer.style.color = isLight ? "#5c6166" : "#bfbdb6";
|
||||
this.diagnosticsContainer.style.padding = "8px";
|
||||
this.diagnosticsContainer.style.fontSize = "13px";
|
||||
this.container.appendChild(this.diagnosticsContainer);
|
||||
}
|
||||
}
|
||||
|
||||
private onContentChange() {
|
||||
// Debounce both workspace update and type checking
|
||||
if (this.checkTimeoutId !== null) {
|
||||
window.clearTimeout(this.checkTimeoutId);
|
||||
}
|
||||
this.checkTimeoutId = window.setTimeout(() => {
|
||||
const content = this.editor?.getValue() ?? "";
|
||||
|
||||
if (this.workspace && this.fileHandle) {
|
||||
try {
|
||||
this.workspace.updateFile(this.fileHandle, content);
|
||||
} catch (err) {
|
||||
console.error("Error updating file:", err);
|
||||
}
|
||||
}
|
||||
|
||||
this.checkFile();
|
||||
}, 150);
|
||||
}
|
||||
|
||||
private checkFile() {
|
||||
if (!this.workspace || !this.fileHandle || !this.editor) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const diagnostics = this.workspace.checkFile(this.fileHandle);
|
||||
const mapped: Diagnostic[] = diagnostics.map((diagnostic) => ({
|
||||
id: diagnostic.id(),
|
||||
message: diagnostic.message(),
|
||||
severity: diagnostic.severity(),
|
||||
range: diagnostic.toRange(this.workspace!) ?? null,
|
||||
raw: diagnostic,
|
||||
}));
|
||||
|
||||
this.updateDiagnostics(mapped);
|
||||
this.hideError();
|
||||
} catch (err) {
|
||||
console.error("Error checking file:", err);
|
||||
this.showError(this.formatError(err));
|
||||
}
|
||||
}
|
||||
|
||||
private updateDiagnostics(diagnostics: Diagnostic[]) {
|
||||
// Update language server diagnostics for code actions
|
||||
if (this.languageServer) {
|
||||
this.languageServer.updateDiagnostics(diagnostics);
|
||||
}
|
||||
|
||||
// Update Monaco markers
|
||||
if (this.editor) {
|
||||
const model = this.editor.getModel();
|
||||
if (model) {
|
||||
monaco.editor.setModelMarkers(
|
||||
model,
|
||||
"owner",
|
||||
diagnostics.map((diagnostic) => {
|
||||
const range = diagnostic.range;
|
||||
return {
|
||||
code: diagnostic.id,
|
||||
startLineNumber: range?.start?.line ?? 1,
|
||||
startColumn: range?.start?.column ?? 1,
|
||||
endLineNumber: range?.end?.line ?? 1,
|
||||
endColumn: range?.end?.column ?? 1,
|
||||
message: diagnostic.message,
|
||||
severity: this.mapSeverity(diagnostic.severity),
|
||||
tags: [],
|
||||
};
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Update diagnostics panel
|
||||
if (this.diagnosticsContainer) {
|
||||
const isLight = this.options.theme === "light";
|
||||
const mutedColor = isLight ? "#8a9199" : "#565b66";
|
||||
|
||||
this.diagnosticsContainer.innerHTML = "";
|
||||
|
||||
if (diagnostics.length > 0) {
|
||||
const list = document.createElement("ul");
|
||||
list.style.margin = "0";
|
||||
list.style.padding = "0";
|
||||
list.style.listStyle = "none";
|
||||
|
||||
diagnostics.forEach((diagnostic) => {
|
||||
const item = this.createDiagnosticItem(diagnostic, mutedColor);
|
||||
list.appendChild(item);
|
||||
});
|
||||
this.diagnosticsContainer.appendChild(list);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private createDiagnosticItem(
|
||||
diagnostic: Diagnostic,
|
||||
mutedColor: string,
|
||||
): HTMLElement {
|
||||
const startLine = diagnostic.range?.start?.line ?? 1;
|
||||
const startColumn = diagnostic.range?.start?.column ?? 1;
|
||||
const isLight = this.options.theme === "light";
|
||||
|
||||
// Error: red, Warning: yellow/orange
|
||||
const severityColor =
|
||||
diagnostic.severity === Severity.Error
|
||||
? isLight
|
||||
? "#e65050"
|
||||
: "#d95757"
|
||||
: diagnostic.severity === Severity.Warning
|
||||
? isLight
|
||||
? "#f2ae49"
|
||||
: "#e6b450"
|
||||
: mutedColor;
|
||||
|
||||
const item = document.createElement("li");
|
||||
item.style.marginBottom = "8px";
|
||||
item.style.paddingLeft = "8px";
|
||||
item.style.borderLeft = `3px solid ${severityColor}`;
|
||||
|
||||
const button = document.createElement("button");
|
||||
button.style.all = "unset";
|
||||
button.style.width = "100%";
|
||||
button.style.textAlign = "left";
|
||||
button.style.cursor = "pointer";
|
||||
button.style.userSelect = "text";
|
||||
|
||||
button.innerHTML = `
|
||||
<span style="color: ${severityColor}; font-weight: 500;">${diagnostic.id}</span>
|
||||
<span style="color: ${mutedColor}; margin-left: 8px;">${startLine}:${startColumn}</span>
|
||||
<div style="margin-top: 2px;">${diagnostic.message}</div>
|
||||
`;
|
||||
|
||||
button.addEventListener("click", () => {
|
||||
if (diagnostic.range && this.editor) {
|
||||
const range = diagnostic.range;
|
||||
this.editor.revealRange({
|
||||
startLineNumber: range.start.line,
|
||||
startColumn: range.start.column,
|
||||
endLineNumber: range.end.line,
|
||||
endColumn: range.end.column,
|
||||
});
|
||||
this.editor.setSelection({
|
||||
startLineNumber: range.start.line,
|
||||
startColumn: range.start.column,
|
||||
endLineNumber: range.end.line,
|
||||
endColumn: range.end.column,
|
||||
});
|
||||
this.editor.focus();
|
||||
}
|
||||
});
|
||||
|
||||
item.appendChild(button);
|
||||
return item;
|
||||
}
|
||||
|
||||
private showError(message: string) {
|
||||
if (!this.errorContainer) {
|
||||
this.errorContainer = document.createElement("div");
|
||||
this.errorContainer.style.position = "absolute";
|
||||
this.errorContainer.style.bottom = "10px";
|
||||
this.errorContainer.style.left = "10px";
|
||||
this.errorContainer.style.right = "10px";
|
||||
this.errorContainer.style.padding = "12px";
|
||||
this.errorContainer.style.backgroundColor = "#ff4444";
|
||||
this.errorContainer.style.color = "#fff";
|
||||
this.errorContainer.style.borderRadius = "4px";
|
||||
this.container.style.position = "relative";
|
||||
this.container.appendChild(this.errorContainer);
|
||||
}
|
||||
this.errorContainer.textContent = message;
|
||||
this.errorContainer.style.display = "block";
|
||||
}
|
||||
|
||||
private hideError() {
|
||||
if (this.errorContainer) {
|
||||
this.errorContainer.style.display = "none";
|
||||
}
|
||||
}
|
||||
|
||||
private formatError(error: unknown): string {
|
||||
const message = error instanceof Error ? error.message : `${error}`;
|
||||
return message.startsWith("Error: ")
|
||||
? message.slice("Error: ".length)
|
||||
: message;
|
||||
}
|
||||
|
||||
private mapSeverity(severity: Severity): monaco.MarkerSeverity {
|
||||
switch (severity) {
|
||||
case Severity.Info:
|
||||
return monaco.MarkerSeverity.Info;
|
||||
case Severity.Warning:
|
||||
return monaco.MarkerSeverity.Warning;
|
||||
case Severity.Error:
|
||||
return monaco.MarkerSeverity.Error;
|
||||
case Severity.Fatal:
|
||||
return monaco.MarkerSeverity.Error;
|
||||
}
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.languageServer?.dispose();
|
||||
this.editor?.dispose();
|
||||
if (this.workspace && this.fileHandle) {
|
||||
try {
|
||||
this.workspace.closeFile(this.fileHandle);
|
||||
} catch (err) {
|
||||
console.warn("Error closing file:", err);
|
||||
}
|
||||
}
|
||||
if (this.checkTimeoutId !== null) {
|
||||
window.clearTimeout(this.checkTimeoutId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class LanguageServer
|
||||
implements
|
||||
monaco.languages.DefinitionProvider,
|
||||
monaco.languages.HoverProvider,
|
||||
monaco.languages.CompletionItemProvider,
|
||||
monaco.languages.InlayHintsProvider,
|
||||
monaco.languages.CodeActionProvider
|
||||
{
|
||||
private definitionDisposable: monaco.IDisposable;
|
||||
private hoverDisposable: monaco.IDisposable;
|
||||
private completionDisposable: monaco.IDisposable;
|
||||
private inlayHintsDisposable: monaco.IDisposable;
|
||||
private codeActionDisposable: monaco.IDisposable;
|
||||
private diagnostics: Diagnostic[] = [];
|
||||
|
||||
constructor(
|
||||
private workspace: Workspace,
|
||||
private fileHandle: FileHandle,
|
||||
) {
|
||||
this.definitionDisposable = monaco.languages.registerDefinitionProvider(
|
||||
"python",
|
||||
this,
|
||||
);
|
||||
this.hoverDisposable = monaco.languages.registerHoverProvider(
|
||||
"python",
|
||||
this,
|
||||
);
|
||||
this.completionDisposable =
|
||||
monaco.languages.registerCompletionItemProvider("python", this);
|
||||
this.inlayHintsDisposable = monaco.languages.registerInlayHintsProvider(
|
||||
"python",
|
||||
this,
|
||||
);
|
||||
this.codeActionDisposable = monaco.languages.registerCodeActionProvider(
|
||||
"python",
|
||||
this,
|
||||
);
|
||||
}
|
||||
|
||||
updateDiagnostics(diagnostics: Diagnostic[]) {
|
||||
this.diagnostics = diagnostics;
|
||||
}
|
||||
|
||||
triggerCharacters = ["."];
|
||||
|
||||
provideCompletionItems(
|
||||
_model: monaco.editor.ITextModel,
|
||||
position: monaco.Position,
|
||||
): monaco.languages.ProviderResult<monaco.languages.CompletionList> {
|
||||
try {
|
||||
const completions = this.workspace.completions(
|
||||
this.fileHandle,
|
||||
new TyPosition(position.lineNumber, position.column),
|
||||
);
|
||||
|
||||
const digitsLength = String(completions.length - 1).length;
|
||||
|
||||
return {
|
||||
suggestions: completions.map((completion, i) => ({
|
||||
label: {
|
||||
label: completion.name,
|
||||
detail:
|
||||
completion.module_name == null
|
||||
? undefined
|
||||
: ` (import ${completion.module_name})`,
|
||||
description: completion.detail ?? undefined,
|
||||
},
|
||||
sortText: String(i).padStart(digitsLength, "0"),
|
||||
kind:
|
||||
completion.kind == null
|
||||
? monaco.languages.CompletionItemKind.Variable
|
||||
: mapCompletionKind(completion.kind),
|
||||
insertText: completion.insert_text ?? completion.name,
|
||||
additionalTextEdits: completion.additional_text_edits?.map(
|
||||
(edit: TextEdit) => ({
|
||||
range: tyRangeToMonacoRange(edit.range),
|
||||
text: edit.new_text,
|
||||
}),
|
||||
),
|
||||
documentation: completion.documentation,
|
||||
detail: completion.detail,
|
||||
range: undefined as any,
|
||||
})),
|
||||
};
|
||||
} catch (err) {
|
||||
console.warn("Error providing completions:", err);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
provideHover(
|
||||
_model: monaco.editor.ITextModel,
|
||||
position: monaco.Position,
|
||||
): monaco.languages.ProviderResult<monaco.languages.Hover> {
|
||||
try {
|
||||
const hover = this.workspace.hover(
|
||||
this.fileHandle,
|
||||
new TyPosition(position.lineNumber, position.column),
|
||||
);
|
||||
|
||||
if (hover == null) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
range: tyRangeToMonacoRange(hover.range),
|
||||
contents: [{ value: hover.markdown, isTrusted: true }],
|
||||
};
|
||||
} catch (err) {
|
||||
console.warn("Error providing hover:", err);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
provideDefinition(
|
||||
model: monaco.editor.ITextModel,
|
||||
position: monaco.Position,
|
||||
): monaco.languages.ProviderResult<
|
||||
monaco.languages.Definition | monaco.languages.LocationLink[]
|
||||
> {
|
||||
try {
|
||||
const links = this.workspace.gotoDefinition(
|
||||
this.fileHandle,
|
||||
new TyPosition(position.lineNumber, position.column),
|
||||
);
|
||||
|
||||
if (links.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const currentUri = model.uri;
|
||||
const results = links
|
||||
.filter((link: LocationLink) => {
|
||||
const linkUri = monaco.Uri.parse(link.path);
|
||||
return linkUri.path === currentUri.path;
|
||||
})
|
||||
.map((link: LocationLink) => ({
|
||||
uri: currentUri,
|
||||
range: tyRangeToMonacoRange(link.full_range),
|
||||
targetSelectionRange:
|
||||
link.selection_range == null
|
||||
? undefined
|
||||
: tyRangeToMonacoRange(link.selection_range),
|
||||
originSelectionRange:
|
||||
link.origin_selection_range == null
|
||||
? undefined
|
||||
: tyRangeToMonacoRange(link.origin_selection_range),
|
||||
}));
|
||||
|
||||
return results.length > 0 ? results : undefined;
|
||||
} catch (err) {
|
||||
console.warn("Error providing definition:", err);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
provideInlayHints(
|
||||
_model: monaco.editor.ITextModel,
|
||||
range: monaco.IRange,
|
||||
): monaco.languages.ProviderResult<monaco.languages.InlayHintList> {
|
||||
try {
|
||||
const inlayHints = this.workspace.inlayHints(
|
||||
this.fileHandle,
|
||||
monacoRangeToTyRange(range),
|
||||
);
|
||||
|
||||
if (inlayHints.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
dispose: () => {},
|
||||
hints: inlayHints.map((hint) => ({
|
||||
label: hint.label.map((part) => ({
|
||||
label: part.label,
|
||||
})),
|
||||
position: {
|
||||
lineNumber: hint.position.line,
|
||||
column: hint.position.column,
|
||||
},
|
||||
kind: mapInlayHintKind(hint.kind),
|
||||
textEdits: hint.text_edits.map((edit: TextEdit) => ({
|
||||
range: tyRangeToMonacoRange(edit.range),
|
||||
text: edit.new_text,
|
||||
})),
|
||||
})),
|
||||
};
|
||||
} catch (err) {
|
||||
console.warn("Error providing inlay hints:", err);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
provideCodeActions(
|
||||
model: monaco.editor.ITextModel,
|
||||
range: monaco.Range,
|
||||
): monaco.languages.ProviderResult<monaco.languages.CodeActionList> {
|
||||
const actions: monaco.languages.CodeAction[] = [];
|
||||
|
||||
for (const diagnostic of this.diagnostics) {
|
||||
const diagnosticRange = diagnostic.range;
|
||||
if (diagnosticRange == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const monacoRange = tyRangeToMonacoRange(diagnosticRange);
|
||||
if (!monaco.Range.areIntersecting(range, new monaco.Range(
|
||||
monacoRange.startLineNumber,
|
||||
monacoRange.startColumn,
|
||||
monacoRange.endLineNumber,
|
||||
monacoRange.endColumn,
|
||||
))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const codeActions = this.workspace.codeActions(
|
||||
this.fileHandle,
|
||||
diagnostic.raw,
|
||||
);
|
||||
if (codeActions == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const codeAction of codeActions) {
|
||||
actions.push({
|
||||
title: codeAction.title,
|
||||
kind: "quickfix",
|
||||
isPreferred: codeAction.preferred,
|
||||
edit: {
|
||||
edits: codeAction.edits.map((edit) => ({
|
||||
resource: model.uri,
|
||||
textEdit: {
|
||||
range: tyRangeToMonacoRange(edit.range),
|
||||
text: edit.new_text,
|
||||
},
|
||||
versionId: model.getVersionId(),
|
||||
})),
|
||||
},
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn("Error getting code actions:", err);
|
||||
}
|
||||
}
|
||||
|
||||
if (actions.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
actions,
|
||||
dispose: () => {},
|
||||
};
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.definitionDisposable.dispose();
|
||||
this.hoverDisposable.dispose();
|
||||
this.completionDisposable.dispose();
|
||||
this.inlayHintsDisposable.dispose();
|
||||
this.codeActionDisposable.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
function tyRangeToMonacoRange(range: TyRange): monaco.IRange {
|
||||
return {
|
||||
startLineNumber: range.start.line,
|
||||
startColumn: range.start.column,
|
||||
endLineNumber: range.end.line,
|
||||
endColumn: range.end.column,
|
||||
};
|
||||
}
|
||||
|
||||
function monacoRangeToTyRange(range: monaco.IRange): TyRange {
|
||||
return new TyRange(
|
||||
new TyPosition(range.startLineNumber, range.startColumn),
|
||||
new TyPosition(range.endLineNumber, range.endColumn),
|
||||
);
|
||||
}
|
||||
|
||||
function mapInlayHintKind(kind: InlayHintKind): monaco.languages.InlayHintKind {
|
||||
switch (kind) {
|
||||
case InlayHintKind.Type:
|
||||
return monaco.languages.InlayHintKind.Type;
|
||||
case InlayHintKind.Parameter:
|
||||
return monaco.languages.InlayHintKind.Parameter;
|
||||
}
|
||||
}
|
||||
|
||||
function mapCompletionKind(
|
||||
kind: CompletionKind,
|
||||
): monaco.languages.CompletionItemKind {
|
||||
switch (kind) {
|
||||
case CompletionKind.Text:
|
||||
return monaco.languages.CompletionItemKind.Text;
|
||||
case CompletionKind.Method:
|
||||
return monaco.languages.CompletionItemKind.Method;
|
||||
case CompletionKind.Function:
|
||||
return monaco.languages.CompletionItemKind.Function;
|
||||
case CompletionKind.Constructor:
|
||||
return monaco.languages.CompletionItemKind.Constructor;
|
||||
case CompletionKind.Field:
|
||||
return monaco.languages.CompletionItemKind.Field;
|
||||
case CompletionKind.Variable:
|
||||
return monaco.languages.CompletionItemKind.Variable;
|
||||
case CompletionKind.Class:
|
||||
return monaco.languages.CompletionItemKind.Class;
|
||||
case CompletionKind.Interface:
|
||||
return monaco.languages.CompletionItemKind.Interface;
|
||||
case CompletionKind.Module:
|
||||
return monaco.languages.CompletionItemKind.Module;
|
||||
case CompletionKind.Property:
|
||||
return monaco.languages.CompletionItemKind.Property;
|
||||
case CompletionKind.Unit:
|
||||
return monaco.languages.CompletionItemKind.Unit;
|
||||
case CompletionKind.Value:
|
||||
return monaco.languages.CompletionItemKind.Value;
|
||||
case CompletionKind.Enum:
|
||||
return monaco.languages.CompletionItemKind.Enum;
|
||||
case CompletionKind.Keyword:
|
||||
return monaco.languages.CompletionItemKind.Keyword;
|
||||
case CompletionKind.Snippet:
|
||||
return monaco.languages.CompletionItemKind.Snippet;
|
||||
case CompletionKind.Color:
|
||||
return monaco.languages.CompletionItemKind.Color;
|
||||
case CompletionKind.File:
|
||||
return monaco.languages.CompletionItemKind.File;
|
||||
case CompletionKind.Reference:
|
||||
return monaco.languages.CompletionItemKind.Reference;
|
||||
case CompletionKind.Folder:
|
||||
return monaco.languages.CompletionItemKind.Folder;
|
||||
case CompletionKind.EnumMember:
|
||||
return monaco.languages.CompletionItemKind.EnumMember;
|
||||
case CompletionKind.Constant:
|
||||
return monaco.languages.CompletionItemKind.Constant;
|
||||
case CompletionKind.Struct:
|
||||
return monaco.languages.CompletionItemKind.Struct;
|
||||
case CompletionKind.Event:
|
||||
return monaco.languages.CompletionItemKind.Event;
|
||||
case CompletionKind.Operator:
|
||||
return monaco.languages.CompletionItemKind.Operator;
|
||||
case CompletionKind.TypeParameter:
|
||||
return monaco.languages.CompletionItemKind.TypeParameter;
|
||||
}
|
||||
}
|
||||
|
||||
44
playground/ty-embed/src/index.css
Normal file
44
playground/ty-embed/src/index.css
Normal file
@@ -0,0 +1,44 @@
|
||||
/* Basic reset for the editor container */
|
||||
.ty-embed-container {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
||||
"Helvetica Neue", Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.ty-embed-container *,
|
||||
.ty-embed-container *::before,
|
||||
.ty-embed-container *::after {
|
||||
box-sizing: inherit;
|
||||
}
|
||||
|
||||
/* Loading state */
|
||||
.ty-embed-loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 40px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* Error message */
|
||||
.ty-embed-error {
|
||||
padding: 12px;
|
||||
background-color: #ff4444;
|
||||
color: #fff;
|
||||
border-radius: 4px;
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
/* Diagnostics panel */
|
||||
.ty-embed-diagnostics {
|
||||
font-family: "Monaco", "Menlo", "Ubuntu Mono", "Consolas", "source-code-pro",
|
||||
monospace;
|
||||
}
|
||||
|
||||
.ty-embed-diagnostic-item {
|
||||
transition: background-color 0.15s ease;
|
||||
}
|
||||
|
||||
.ty-embed-diagnostic-item:hover {
|
||||
filter: brightness(0.95);
|
||||
}
|
||||
85
playground/ty-embed/src/index.ts
Normal file
85
playground/ty-embed/src/index.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import { EmbeddableEditor, EditorOptions } from "./EmbeddableEditor";
|
||||
|
||||
export interface TyEditorOptions extends EditorOptions {
|
||||
container: HTMLElement | string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize a ty editor instance in the specified container.
|
||||
*
|
||||
* @param options Configuration options for the editor
|
||||
* @returns A handle to control the editor instance
|
||||
*
|
||||
* @example
|
||||
* ```javascript
|
||||
* import { createTyEditor } from 'ty-embed';
|
||||
*
|
||||
* const editor = createTyEditor({
|
||||
* container: '#editor',
|
||||
* initialCode: 'print("Hello, ty!")',
|
||||
* theme: 'dark',
|
||||
* height: '500px'
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export function createTyEditor(options: TyEditorOptions) {
|
||||
const editor = new EmbeddableEditor(options.container, options);
|
||||
|
||||
return {
|
||||
/**
|
||||
* Unmount and cleanup the editor instance
|
||||
*/
|
||||
dispose() {
|
||||
editor.dispose();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize multiple ty editor instances at once.
|
||||
*
|
||||
* @param selector CSS selector for containers (e.g., '.ty-editor')
|
||||
* @param defaultOptions Default options to apply to all editors
|
||||
* @returns Array of editor handles
|
||||
*
|
||||
* @example
|
||||
* ```html
|
||||
* <div class="ty-editor" data-code="print('Editor 1')"></div>
|
||||
* <div class="ty-editor" data-code="print('Editor 2')"></div>
|
||||
*
|
||||
* <script>
|
||||
* createTyEditors('.ty-editor', { theme: 'dark' });
|
||||
* </script>
|
||||
* ```
|
||||
*/
|
||||
export function createTyEditors(
|
||||
selector: string,
|
||||
defaultOptions: Partial<EditorOptions> = {},
|
||||
) {
|
||||
const containers = document.querySelectorAll(selector);
|
||||
const editors = [];
|
||||
|
||||
for (const container of Array.from(containers)) {
|
||||
const dataCode = container.getAttribute("data-code");
|
||||
const dataTheme = container.getAttribute("data-theme");
|
||||
const dataHeight = container.getAttribute("data-height");
|
||||
const dataFile = container.getAttribute("data-file");
|
||||
|
||||
const options: TyEditorOptions = {
|
||||
container: container as HTMLElement,
|
||||
...defaultOptions,
|
||||
initialCode: dataCode ?? defaultOptions.initialCode,
|
||||
theme:
|
||||
(dataTheme as "light" | "dark") ?? defaultOptions.theme ?? "light",
|
||||
height: dataHeight ?? defaultOptions.height ?? "400px",
|
||||
fileName: dataFile ?? defaultOptions.fileName,
|
||||
};
|
||||
|
||||
editors.push(createTyEditor(options));
|
||||
}
|
||||
|
||||
return editors;
|
||||
}
|
||||
|
||||
// Re-export types
|
||||
export type { EditorOptions };
|
||||
9
playground/ty-embed/tsconfig.json
Normal file
9
playground/ty-embed/tsconfig.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"declaration": true,
|
||||
"declarationDir": "./dist/types"
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
43
playground/ty-embed/vite.config.ts
Normal file
43
playground/ty-embed/vite.config.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { defineConfig } from "vite";
|
||||
import { viteStaticCopy } from "vite-plugin-static-copy";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
viteStaticCopy({
|
||||
targets: [
|
||||
{
|
||||
src: ["ty_wasm/*", "!ty_wasm/.gitignore"],
|
||||
dest: "ty_wasm",
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
build: {
|
||||
lib: {
|
||||
entry: "./src/index.ts",
|
||||
name: "TyEmbed",
|
||||
formats: ["es", "umd"],
|
||||
fileName: (format) => `ty-embed.${format}.js`,
|
||||
},
|
||||
rollupOptions: {
|
||||
external: ["ty_wasm"],
|
||||
output: {
|
||||
assetFileNames: (assetInfo) => {
|
||||
if (assetInfo.name === "style.css") return "ty-embed.css";
|
||||
return assetInfo.name ?? "asset";
|
||||
},
|
||||
paths: {
|
||||
ty_wasm: "./ty_wasm/ty_wasm.js",
|
||||
},
|
||||
},
|
||||
},
|
||||
copyPublicDir: false,
|
||||
},
|
||||
optimizeDeps: {
|
||||
exclude: ["ty_wasm"],
|
||||
},
|
||||
server: {
|
||||
port: 3001,
|
||||
open: "/example-dev.html",
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user