Compare commits

..

12 Commits

Author SHA1 Message Date
David Peter
c3d225fe61 [ty] Embeddable ty playground 2025-12-05 08:51:54 +01:00
Andrew Gallant
fdcb5a7e73 [ty] Clarify the use of SymbolKind in auto-import 2025-12-04 13:21:26 -05:00
Andrew Gallant
6a025d1925 [ty] Redact ranking of completions from e2e LSP tests
I think changes to this value are generally noise. It's hard to tell
what it means and it isn't especially actionable. We already have an
eval running in CI for completion ranking, so I don't think it's
terribly important to care about ranking here in e2e tests _generally_.
2025-12-04 13:21:26 -05:00
Andrew Gallant
f054e7edf8 [ty] Tweaks tests to use clearer language
A completion lacking a module reference doesn't necessarily mean that
the symbol is defined within the current module. I believe the intent
here is that it means that no import is required to use it.
2025-12-04 13:21:26 -05:00
Andrew Gallant
e154efa229 [ty] Update evaluation results
These are all improvements here with one slight regression on
`reveal_type` ranking. The previous completions offered were:

```
$ cargo r -q -p ty_completion_eval show-one ty-extensions-lower-stdlib
ENOTRECOVERABLE (module: errno)
REG_WHOLE_HIVE_VOLATILE (module: winreg)
SQLITE_NOTICE_RECOVER_WAL (module: _sqlite3)
SupportsGetItemViewable (module: _typeshed)
removeHandler (module: unittest.signals)
reveal_mro (module: ty_extensions)
reveal_protocol_interface (module: ty_extensions)
reveal_type (module: typing) (*, 8/10)
_remove_original_values (module: _osx_support)
_remove_universal_flags (module: _osx_support)
-----
found 10 completions
```

And now they are:

```
$ cargo r -q -p ty_completion_eval show-one ty-extensions-lower-stdlib
ENOTRECOVERABLE (module: errno)
REG_WHOLE_HIVE_VOLATILE (module: winreg)
SQLITE_NOTICE_RECOVER_WAL (module: sqlite3)
SQLITE_NOTICE_RECOVER_WAL (module: sqlite3.dbapi2)
removeHandler (module: unittest)
removeHandler (module: unittest.signals)
reveal_mro (module: ty_extensions)
reveal_protocol_interface (module: ty_extensions)
reveal_type (module: typing) (*, 9/9)
-----
found 9 completions
```

Some completions were removed (because they are now considered
unexported) and some were added (likely do to better re-export support).

This particular case probably warrants more special attention anyway.
So I think this is fine. (It's only a one-ranking regression.)
2025-12-04 13:21:26 -05:00
Andrew Gallant
32f400a457 [ty] Make auto-import ignore symbols in modules starting with a _
This applies recursively. So if *any* component of a module name starts
with a `_`, then symbols from that module are excluded from auto-import.

The exception is when it's a module within first party code. Then we
want to include it in auto-import.
2025-12-04 13:21:26 -05:00
Andrew Gallant
2a38395bc8 [ty] Add some tests for re-exports and __all__ to completions
Note that the `Deprecated` symbols from `importlib.metadata` are no
longer offered because 1) `importlib.metadata` defined `__all__` and 2)
the `Deprecated` symbols aren't in it. These seem to not be a part of
its public API according to the docs, so this seems right to me.
2025-12-04 13:21:26 -05:00
Andrew Gallant
8c72b296c9 [ty] Add support for re-exports and __all__ to auto-import
This commit (mostly) re-implements the support for `__all__` in
ty-proper, but inside the auto-import AST scanner.

When `__all__` isn't present in a module, we fall back to conventions to
determine whether a symbol is exported or not:
https://docs.python.org/3/library/index.html

However, in keeping with current practice for non-auto-import
completions, we continue to provide sunder and dunder names as
re-exports.

When `__all__` is present, we respect it strictly. That is, a symbol is
exported *if and only if* it's in `__all__`. This is somewhat stricter
than pylance seemingly is. I felt like it was a good idea to start here,
and we can relax it based on user demand (perhaps through a setting).
2025-12-04 13:21:26 -05:00
Andrew Gallant
086f1e0b89 [ty] Skip over expressions in auto-import AST scanning 2025-12-04 13:21:26 -05:00
Andrew Gallant
5da45f8ec7 [ty] Simplify auto-import AST visitor slightly and add tests
This simplifies the existing visitor by DRYing it up slightly.
We also add tests for the existing functionality. In particular,
we want to add support for re-export conventions, and that
warrants more careful testing.
2025-12-04 13:21:26 -05:00
Andrew Gallant
62f20b1e86 [ty] Re-arrange imports in symbol extraction
I like using a qualified `ast::` prefix for things from
`ruff_python_ast`, so switch over to that convention.
2025-12-04 13:21:26 -05:00
Aria Desires
cccb0bbaa4 [ty] Add tests for implicit submodule references (#21793)
## Summary

I realized we don't really test `DefinitionKind::ImportFromSubmodule` in
the IDE at all, so here's a bunch of them, just recording our current
behaviour.

## Test Plan

*stares at the camera*
2025-12-04 15:46:23 +00:00
27 changed files with 4314 additions and 116 deletions

View File

@@ -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
1 name file index rank
11 import-deprioritizes-type_check_only main.py 3 2
12 import-deprioritizes-type_check_only main.py 4 3
13 import-keyword-completion main.py 0 1
14 internal-typeshed-hidden main.py 0 4 2
15 none-completion main.py 0 2
16 numpy-array main.py 0 159
17 numpy-array main.py 1 1
18 object-attr-instance-methods main.py 0 1
19 object-attr-instance-methods main.py 1 1
23 scope-prioritize-closer main.py 0 2
24 scope-simple-long-identifier main.py 0 1
25 tstring-completions main.py 0 1
26 ty-extensions-lower-stdlib main.py 0 8 9
27 type-var-typing-over-ast main.py 0 3
28 type-var-typing-over-ast main.py 1 275 239

View File

@@ -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 {

View File

@@ -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

View File

@@ -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
| ^^^^^^
|
");
}
}

View File

@@ -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)

View File

@@ -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) =

View File

@@ -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;

View File

@@ -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

View File

@@ -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(_))
}

View File

@@ -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(())
}

View File

@@ -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": [
{

View File

@@ -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": [
{

View File

@@ -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": [
{

View File

@@ -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": [
{

View File

@@ -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"

View File

@@ -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
View File

@@ -0,0 +1,5 @@
node_modules
dist
ty_wasm
.DS_Store
*.log

View 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.

View 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>

View 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>

View 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"
}
}

View 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;
}
}

View 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);
}

View 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 };

View File

@@ -0,0 +1,9 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"outDir": "./dist",
"declaration": true,
"declarationDir": "./dist/types"
},
"include": ["src"]
}

View 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",
},
});