plum

#treesitter#compiler#wasm

git clone https://git.pyrossh.dev/plum

A statically typed, imperative programming language inspired by rust, python


532b997 — Peter John 2026-11-02T18:33:22+05:30
Move the game-engine std modules (Game, Scene, Color, Vec2) to plum-std/game; LSP std index is recursive
README.md CHANGED
@@ -1043,9 +1043,9 @@ Games run in the browser on Plum's own WebGPU renderer (`plum-game/web/plum_brid
1043
1043
 
1044
1044
  `plum-examples/game.plum` is a small arcade game: steer a ship with the arrow keys or WASD, collect orbs, dodge mines (Enter restarts). It draws shapes, text, additive orb glow (`blendMode(Blend.Add)`) and an animated sprite sheet (`plum-examples/assets/sprites.png`, generated by `plum-examples/tools/make_sprites.py`) and plays music and sound effects (`plum-examples/assets/*.wav`, generated by `plum-examples/tools/make_sounds.py`); both generators live outside the asset dir so they are not shipped. For a bigger game built on this engine see the Knights and Merchants port in the sibling repo `../kam_plum` (`./run.sh` there). Its rules are plain functions with `test` blocks, so `plum test plum-examples` covers them without a browser.
1045
1045
 
1046
- **`std/Scene`** is a scene-graph library written in Plum (see [`plum-examples/scene.plum`](plum-examples/scene.plum)): a tree of `Node`s (group, sprite, rect, circle, text) under a `Stage`, with parent-relative position, rotation, scale and colour, per-node `act` behaviours, a camera and hit testing; its demo renders a minimap into a texture with `textureCreate` and `renderTarget`. Use it for UI and other hierarchical scenes, where moving a parent should move everything in it. For a large world, keep the game objects in flat records and lists instead (for example `enum Entity = Unit | House` with an `update` and a `draw` that the enum dispatches to its variants) and draw them directly through `std/Game`; a tree of thousands of nodes costs a closure call and a matrix composition per node every frame.
1046
+ **`std/game/Scene`** is a scene-graph library written in Plum (see [`plum-examples/scene.plum`](plum-examples/scene.plum)): a tree of `Node`s (group, sprite, rect, circle, text) under a `Stage`, with parent-relative position, rotation, scale and colour, per-node `act` behaviours, a camera and hit testing; its demo renders a minimap into a texture with `textureCreate` and `renderTarget`. Use it for UI and other hierarchical scenes, where moving a parent should move everything in it. For a large world, keep the game objects in flat records and lists instead (for example `enum Entity = Unit | House` with an `update` and a `draw` that the enum dispatches to its variants) and draw them directly through `std/game/Game`; a tree of thousands of nodes costs a closure call and a matrix composition per node every frame.
1047
1047
 
1048
- `std/Scene` follows libGDX Scene2D for draw order and culling. A node's z-index is its position in its parent's child list (draw order is list order, hit testing goes in reverse):
1048
+ `std/game/Scene` follows libGDX Scene2D for draw order and culling. A node's z-index is its position in its parent's child list (draw order is list order, hit testing goes in reverse):
1049
1049
 
1050
1050
  ```plum
1051
1051
  n.zIndex() # index among its siblings, -1 without a parent
@@ -1071,7 +1071,7 @@ export fun update(g: G, dt: Float)
1071
1071
  export fun draw(g: G)
1072
1072
  ```
1073
1073
 
1074
- The page calls `init` once after the assets are loaded, then `update` and `draw` every frame. A game imports `std/Game`, `std/Vec2` and `std/Color`; pure logic can import `std/Vec2` and `std/Color` alone. Under `plum test` and `plum run` the host functions are stubs that trap with `host function 'X' is not available here`, so `test` blocks work as long as they don't call the host.
1074
+ The page calls `init` once after the assets are loaded, then `update` and `draw` every frame. A game imports `std/game/Game`, `std/game/Vec2` and `std/game/Color`; pure logic can import `std/game/Vec2` and `std/game/Color` alone. Under `plum test` and `plum run` the host functions are stubs that trap with `host function 'X' is not available here`, so `test` blocks work as long as they don't call the host.
1075
1075
 
1076
1076
  **Headless tests:** the `plum-game` crate (a standalone crate, wasmtime only) runs a compiled game with no browser against a `Recorder` backend that logs every host call as a string and answers input from scripted values, so `cargo test --manifest-path plum-game/Cargo.toml` checks games' draw-call sequences (`plum-game/tests/host_test.rs`). `plum-game <wasm> [--assets DIR] [--frames N]` does the same from the command line and prints the host calls of the last frame.
1077
1077
 
@@ -1081,7 +1081,7 @@ The page calls `init` once after the assets are loaded, then `update` and `draw`
1081
1081
 
1082
1082
  **Sounds:** the browser decodes every `.wav` and `.ogg` file of the asset root before `init`, and `soundLoad` looks the decoded sound up by path with the same path rules as `textureLoad`. Browsers only start audio after a user gesture (a key press or click); sounds played before that are silent, and the music of the example begins once you press a key.
1083
1083
 
1084
- **Host API** (`std/Game`; texture and sound handles are `-1` on failure; coordinates are world units once a camera is set):
1084
+ **Host API** (`std/game/Game`; texture and sound handles are `-1` on failure; coordinates are world units once a camera is set):
1085
1085
 
1086
1086
  | function | meaning |
1087
1087
  |---|---|
plum-cli/src/main.rs CHANGED
@@ -605,7 +605,7 @@ fn hostImports(
605
605
  },
606
606
  )))
607
607
  }
608
- // A `plum` import this host doesn't define (the game API of `std/Game`)
608
+ // A `plum` import this host doesn't define (the game API of `std/game/Game`)
609
609
  // is stubbed with a function that traps when called, so a program
610
610
  // that imports it can still run or test code that never calls it.
611
611
  ("plum", n) => {
plum-cli/tests/host_stub_test.rs CHANGED
@@ -36,7 +36,7 @@ fn runPlumTest(name: &str, source: &str) -> (bool, String) {
36
36
  fn stubbed_host_import_unused_test_passes() {
37
37
  let (ok, all) = runPlumTest(
38
38
  "pass",
39
- "import std/Int\nimport std/Game\n\ntest \"pure\"\n assert 1 + 1 == 2\n",
39
+ "import std/Int\nimport std/game/Game\n\ntest \"pure\"\n assert 1 + 1 == 2\n",
40
40
  );
41
41
  assert!(ok, "output: {all}");
42
42
  assert!(all.contains("1 passed, 0 failed"), "output: {all}");
@@ -46,7 +46,7 @@ fn stubbed_host_import_unused_test_passes() {
46
46
  fn stubbed_host_import_traps_when_called() {
47
47
  let (ok, all) = runPlumTest(
48
48
  "trap",
49
- "import std/Int\nimport std/Float\nimport std/Game\n\ntest \"calls host\"\n assert screenWidth() > 0.0\n",
49
+ "import std/Int\nimport std/Float\nimport std/game/Game\n\ntest \"calls host\"\n assert screenWidth() > 0.0\n",
50
50
  );
51
51
  assert!(!ok, "output: {all}");
52
52
  assert!(
@@ -59,7 +59,7 @@ fn stubbed_host_import_traps_when_called() {
59
59
  fn real_host_functions_still_work_next_to_stubs() {
60
60
  let (ok, all) = runPlumTest(
61
61
  "real",
62
- "import std/Int\nimport std/Str\nimport std/Os\nimport std/Game\n\ntest \"real\"\n printLn(\"hello\")\n assert 1 == 1\n",
62
+ "import std/Int\nimport std/Str\nimport std/Os\nimport std/game/Game\n\ntest \"real\"\n printLn(\"hello\")\n assert 1 == 1\n",
63
63
  );
64
64
  assert!(ok, "output: {all}");
65
65
  assert!(all.contains("hello"), "output: {all}");
plum-core/tests/loader_test.rs CHANGED
@@ -505,3 +505,29 @@ fn legacySingleSelfVariantIsAnError() {
505
505
  "enum User has a single variant named User — write record User(...)"
506
506
  );
507
507
  }
508
+
509
+ #[test]
510
+ fn nestedStdImportResolvesToASubdirectory() {
511
+ let dir = TempDir::new().unwrap();
512
+ let lib_path = dir.path().join("libs");
513
+ writeLibFile(
514
+ &lib_path,
515
+ "plum-std/game/Util",
516
+ "module std\n\nfun helperFn(b: Bool) -> Bool =\n b\n",
517
+ );
518
+ let entry = writeLibFile(
519
+ &lib_path,
520
+ "main",
521
+ "module fixtures\n\nimport std/game/Util\n\nfun mainFn(b: Bool) -> Bool =\n helperFn(b)\n",
522
+ );
523
+ let source = loadAndMerge(&entry, &lib_path).expect("nested std import loads");
524
+ assert!(source.items.iter().any(
525
+ |i| matches!(i, plum_core::ast::Item::Fn(f) if f.name == "helperFn" && f.file.ends_with("game/Util.plum"))
526
+ ));
527
+ let missing = writeLibFile(
528
+ &lib_path,
529
+ "other",
530
+ "module fixtures\n\nimport std/Util\n\nfun f(b: Bool) -> Bool =\n b\n",
531
+ );
532
+ assert!(loadAndMerge(&missing, &lib_path).is_err());
533
+ }
plum-examples/game.plum CHANGED
@@ -13,9 +13,9 @@ import std/Float
13
13
  import std/List
14
14
  import std/Math
15
15
  import std/Random
16
- import std/Vec2
16
+ import std/game/Vec2
17
- import std/Color
17
+ import std/game/Color
18
- import std/Game
18
+ import std/game/Game
19
19
 
20
20
  primitive Rules =
21
21
  ArenaW = 800.0
plum-examples/scene.plum CHANGED
@@ -1,5 +1,5 @@
1
1
  # Clockwork: a scene-graph demo (`plum game plum-examples/scene.plum`). A sun, four
2
- # planets and their moons orbit by nesting rotating groups (std/Scene): each orbit
2
+ # planets and their moons orbit by nesting rotating groups (std/game/Scene): each orbit
3
3
  # is a group whose rotation carries everything inside it, so a moon only ever
4
4
  # says "I sit 40 units from my planet" and the maths composes the rest. Click a
5
5
  # body to select it (a halo and its name), click again or the background to clear;
@@ -17,10 +17,10 @@ import std/Int
17
17
  import std/Float
18
18
  import std/List
19
19
  import std/Math
20
- import std/Vec2
20
+ import std/game/Vec2
21
- import std/Color
21
+ import std/game/Color
22
- import std/Game
22
+ import std/game/Game
23
- import std/Scene
23
+ import std/game/Scene
24
24
 
25
25
  primitive Layout =
26
26
  Width = 800.0
plum-game/src/api.rs CHANGED
@@ -1,5 +1,5 @@
1
1
  //! The host API a Plum game programs against: one `Backend` method per
2
- //! extern in `plum-std/Game.plum`, in Rust types (`Float` is `f32`, `Int` is
2
+ //! extern in `plum-std/game/Game.plum`, in Rust types (`Float` is `f32`, `Int` is
3
3
  //! `i32`, `Str` is `&str`, a `Color` is `[f32; 4]`, a `Vec2` is `(f32, f32)`).
4
4
  //! `Int` handles are indexes into host vectors, `-1` on failure. The browser
5
5
  //! bridge (`web/plum_bridge.js`) implements the same API; this crate's
plum-game/tests/fixtures/assets_fixture.plum CHANGED
@@ -3,9 +3,9 @@ import std/Int
3
3
  import std/Float
4
4
  import std/Str
5
5
  import std/Os
6
- import std/Vec2
6
+ import std/game/Vec2
7
- import std/Color
7
+ import std/game/Color
8
- import std/Game
8
+ import std/game/Game
9
9
 
10
10
  record State(frames: Int)
11
11
 
plum-game/tests/fixtures/host_fixture.plum CHANGED
@@ -3,9 +3,9 @@ import std/Int
3
3
  import std/Float
4
4
  import std/Str
5
5
  import std/Os
6
- import std/Vec2
6
+ import std/game/Vec2
7
- import std/Color
7
+ import std/game/Color
8
- import std/Game
8
+ import std/game/Game
9
9
 
10
10
  record State(frames: Int)
11
11
 
plum-game/tests/fixtures/target_fixture.plum CHANGED
@@ -2,9 +2,9 @@ module target_fixture
2
2
  import std/Int
3
3
  import std/Float
4
4
  import std/Str
5
- import std/Vec2
5
+ import std/game/Vec2
6
- import std/Color
6
+ import std/game/Color
7
- import std/Game
7
+ import std/game/Game
8
8
 
9
9
  record State(canvas: Int, bad: Int)
10
10
 
plum-game/web-test/BENCHMARK.md CHANGED
@@ -14,7 +14,7 @@ the Plum `Scene` tree). Plan: `docs/plans/2026-11-02-render-experiments.md`.
14
14
  - Machine: Apple M3 Pro (`sysctl machdep.cpu.brand_string`), `arm64` (`uname -m`), macOS 26.6.2, 120 Hz display
15
15
  - Browser: Playwright Chromium build 1248 (156.0.8078.4); headless runs use its `chrome-headless-shell`
16
16
  - Scene (1280x720, camera scale 0.8): 5000 moving rotated sprites (4-frame sheet, source rect + flipX), 500 rects, 200 circles, 50 texts whose number changes every frame. Motion is a function of the frame counter, so every run draws the same frames.
17
- - Fixtures (`fixtures/`): `stress.plum` (immediate, `std/Game`), `stress_cmd.plum` (immediate, `std/GameCmd` command buffer), `stress_scene.plum` (`std/Scene` tree, per-call), `stress_scene_cmd.plum` (`std/SceneCmd` tree, command buffer).
17
+ - Fixtures (`fixtures/`): `stress.plum` (immediate, `std/game/Game`), `stress_cmd.plum` (immediate, `std/GameCmd` command buffer), `stress_scene.plum` (`std/game/Scene` tree, per-call), `stress_scene_cmd.plum` (`std/SceneCmd` tree, command buffer).
18
18
  - Command: `plum-web-test <fixture> --renderer R [--headed] --uncapped --measure 10 --frames-wait 6 --json`. `--uncapped` passes `--disable-gpu-vsync --disable-frame-rate-limit`, so the rAF interval is the work per frame, not the display refresh. 6 s warm-up, then 10 s measured.
19
19
  - Every cell was run twice; the table shows the run with the better average and the range of the two averages. Cells ran back to back on an otherwise idle laptop (no thermal control), so treat differences under about 5% as noise.
20
20
  - Phase split (`update_ms`, `draw_ms`, `render_ms` in the JSON): mean ms per frame measured in the page with `performance.now()` around `update`, `draw` and the rest. The counters exist only while `--measure` runs (`globalThis.__plumMeasure`; a null check per frame otherwise).
@@ -57,7 +57,7 @@ Headless is software rendering: the GPU work runs on the CPU and competes with t
57
57
 
58
58
  | fixture | update ms | draw ms (run 1 / 2) | present ms (waits for vsync) |
59
59
  |---|---|---|---|
60
- | stress (per-call `std/Game`) | 0.009 | 0.814 / 0.809 | 7.51 / 7.52 |
60
+ | stress (per-call `std/game/Game`) | 0.009 | 0.814 / 0.809 | 7.51 / 7.52 |
61
61
  | stress_cmd (`std/GameCmd`) | 0.010 / 0.009 | 0.538 / 0.532 | 7.78 / 7.80 |
62
62
 
63
63
  ## Analysis
@@ -101,7 +101,7 @@ Same machine and method as above (headed, uncapped, `--measure 10 --frames-wait
101
101
  - Interleaved textures: 4000 draw calls became 1 (the bound is ceil(textures / 16)), and the frame time fell from 1.3 ms to 0.45 ms (-65%); render time (encode + submit) from 1.0 ms to 0.15 ms.
102
102
  - Stress: the 16-slot batcher alone cost about 5-8% (the vertex grew from 20 to 24 bytes for the slot), within run-to-run spread; the 50 changing strings used to cost 50 canvas rasterisations plus 50 texture uploads per frame, and the SDF atlas removes both, so the scene is now about 40% faster than before (0.98-1.03 ms against 1.58-1.67 ms) with 1 draw call instead of 52.
103
103
 
104
- ## Scene culling and y-sort (std/Scene)
104
+ ## Scene culling and y-sort (std/game/Scene)
105
105
 
106
106
  Fixture `stress_world.plum`: one world layer with 20,000 sprites in a 4000x2300 world, a 1280x720 view at scale 1 (about 2,000 sprites in view while the camera drifts), 500 sprites walking each frame, and `layer.sortChildren(|a, b| a.y < b.y)` every frame. `World.Culling = 1` also calls `setCullingArea` with the camera view; the "no culling" row is the same file with `Culling = 0`. Same machine and method as above (headed, gpu per-call, `--uncapped --measure 10 --frames-wait 6`); one run per cell, so differences under about 5% are noise.
107
107
 
plum-game/web-test/fixtures/interleave.plum CHANGED
@@ -6,9 +6,9 @@ import std/Str
6
6
  import std/Int
7
7
  import std/Float
8
8
  import std/Array
9
- import std/Vec2
9
+ import std/game/Vec2
10
- import std/Color
10
+ import std/game/Color
11
- import std/Game
11
+ import std/game/Game
12
12
 
13
13
  primitive Scene =
14
14
  Sprites = 4000
plum-game/web-test/fixtures/rendertarget.plum CHANGED
@@ -14,9 +14,9 @@ module rendertarget
14
14
  import std/Str
15
15
  import std/Int
16
16
  import std/Float
17
- import std/Vec2
17
+ import std/game/Vec2
18
- import std/Color
18
+ import std/game/Color
19
- import std/Game
19
+ import std/game/Game
20
20
 
21
21
  record Targets(big: Int, small: Int, glass: Int, t: Int)
22
22
 
plum-game/web-test/fixtures/stress.plum CHANGED
@@ -6,9 +6,9 @@ module stress
6
6
  import std/Str
7
7
  import std/Int
8
8
  import std/Float
9
- import std/Vec2
9
+ import std/game/Vec2
10
- import std/Color
10
+ import std/game/Color
11
- import std/Game
11
+ import std/game/Game
12
12
 
13
13
  primitive Scene =
14
14
  Sprites = 5000
plum-game/web-test/fixtures/stress_scene.plum CHANGED
@@ -1,4 +1,4 @@
1
- # The stress scene of stress.plum built as a Scene2D-like scene graph (std/Scene):
1
+ # The stress scene of stress.plum built as a Scene2D-like scene graph (std/game/Scene):
2
2
  # 5000 sprites in 50 slowly rotating groups, 500 rects, 200 circles and 50 texts
3
3
  # with a changing number, drawn through a camera. Every node moves in its own
4
4
  # `act` behaviour, so each frame runs ~5800 closures and a composed-transform draw.
@@ -7,10 +7,10 @@ import std/Str
7
7
  import std/Int
8
8
  import std/Float
9
9
  import std/List
10
- import std/Vec2
10
+ import std/game/Vec2
11
- import std/Color
11
+ import std/game/Color
12
- import std/Game
12
+ import std/game/Game
13
- import std/Scene
13
+ import std/game/Scene
14
14
 
15
15
  primitive Counts =
16
16
  Sprites = 5000
plum-game/web-test/fixtures/stress_world.plum CHANGED
@@ -1,4 +1,4 @@
1
- # A large y-sorted world through the scene graph (std/Scene): 20000 sprites in one
1
+ # A large y-sorted world through the scene graph (std/game/Scene): 20000 sprites in one
2
2
  # world layer, about 2000 of them in the 1280x720 view. Every frame a few hundred
3
3
  # sprites walk, the layer is y-sorted with `sortChildren`, and (when `Culling` is
4
4
  # 1) the camera view is set as the layer's culling area so only the visible
@@ -9,10 +9,10 @@ import std/Int
9
9
  import std/Float
10
10
  import std/Math
11
11
  import std/List
12
- import std/Vec2
12
+ import std/game/Vec2
13
- import std/Color
13
+ import std/game/Color
14
- import std/Game
14
+ import std/game/Game
15
- import std/Scene
15
+ import std/game/Scene
16
16
 
17
17
  primitive World =
18
18
  Sprites = 20000
plum-game/web-test/fixtures/text_sizes.plum CHANGED
@@ -5,9 +5,9 @@ module text_sizes
5
5
  import std/Str
6
6
  import std/Int
7
7
  import std/Float
8
- import std/Vec2
8
+ import std/game/Vec2
9
- import std/Color
9
+ import std/game/Color
10
- import std/Game
10
+ import std/game/Game
11
11
 
12
12
  record Sizes(t: Int)
13
13
 
plum-game/web/plum_bridge.js CHANGED
@@ -8,7 +8,7 @@
8
8
  // backend is WebGPU where the browser has it; there is no fallback (a page
9
9
  // without navigator.gpu says so).
10
10
  //
11
- // Each std/Game draw extern appends vertices to the batcher directly.
11
+ // Each std/game/Game draw extern appends vertices to the batcher directly.
12
12
  // Plum's `Str` is a GC struct JS can't read: it is read through the module's
13
13
  // `strLen`/`strByte` exports and built with `strNew`/`strSetByte`.
14
14
  (function () {
@@ -48,7 +48,7 @@
48
48
 
49
49
  // --- input ----------------------------------------------------------------------
50
50
 
51
- // KeyboardEvent.code -> GLFW key number (the numbering of `Key` in Game.plum).
51
+ // KeyboardEvent.code -> GLFW key number (the numbering of `Key` in game/Game.plum).
52
52
  function glfwKey(code) {
53
53
  if (code.length === 4 && code.startsWith("Key")) return code.charCodeAt(3);
54
54
  if (code.length === 6 && code.startsWith("Digit")) return code.charCodeAt(5);
plum-lsp/src/completion.rs CHANGED
@@ -446,24 +446,27 @@ pub fn memberItems(
446
446
  items
447
447
  }
448
448
 
449
- /// Maps every top-level name declared anywhere under `<lib_path>/plum-std/*.plum`
449
+ /// Maps every top-level name declared anywhere under `<lib_path>/plum-std/`
450
+ /// (subdirectories included) to that file's `import` path (`Str.plum` ->
450
- /// to that file's `import` path (`Str.plum` -> `"std/Str"`) — built by parsing
451
+ /// `"std/Str"`, `game/Scene.plum` -> `"std/game/Scene"`) — built by parsing
451
452
  /// each file standalone (no import resolution needed just to list names).
452
453
  /// Meant to be computed once and cached by the caller.
453
454
  pub fn stdLibIndex(lib_path: &Path) -> HashMap<String, String> {
454
455
  let mut map = HashMap::new();
455
456
  let std_dir = lib_path.join("plum-std");
457
+ let mut files = Vec::new();
458
+ collectPlumFiles(&std_dir, &mut files);
459
+ files.sort();
460
+ for path in files {
456
- let Ok(entries) = std::fs::read_dir(&std_dir) else {
461
+ let Ok(rel) = path.strip_prefix(&std_dir) else {
457
- return map;
458
- };
459
- for entry in entries.flatten() {
460
- let path = entry.path();
461
- if path.extension().and_then(|e| e.to_str()) != Some("plum") {
462
- continue;
463
- }
464
- let Some(stem) = path.file_stem().and_then(|s| s.to_str()) else {
465
462
  continue;
466
463
  };
464
+ let rel = rel.with_extension("");
465
+ let parts: Vec<String> = rel
466
+ .components()
467
+ .map(|c| c.as_os_str().to_string_lossy().into_owned())
468
+ .collect();
469
+ let import_path = format!("std/{}", parts.join("/"));
467
470
  let Ok(text) = std::fs::read_to_string(&path) else {
468
471
  continue;
469
472
  };
@@ -478,7 +481,6 @@ pub fn stdLibIndex(lib_path: &Path) -> HashMap<String, String> {
478
481
  continue;
479
482
  };
480
483
  let source = plum_core::AstParser::new(&text).parseSource(tree.root_node());
481
- let import_path = format!("std/{stem}");
482
484
  for item in &source.items {
483
485
  let name = match item {
484
486
  ast::Item::Fn(f) if f.type_param.is_none() => Some(f.name.clone()),
@@ -507,6 +509,21 @@ pub fn stdLibIndex(lib_path: &Path) -> HashMap<String, String> {
507
509
  map
508
510
  }
509
511
 
512
+ /// Every `.plum` file under `dir`, recursively.
513
+ fn collectPlumFiles(dir: &Path, out: &mut Vec<std::path::PathBuf>) {
514
+ let Ok(entries) = std::fs::read_dir(dir) else {
515
+ return;
516
+ };
517
+ for entry in entries.flatten() {
518
+ let path = entry.path();
519
+ if path.is_dir() {
520
+ collectPlumFiles(&path, out);
521
+ } else if path.extension().and_then(|e| e.to_str()) == Some("plum") {
522
+ out.push(path);
523
+ }
524
+ }
525
+ }
526
+
510
527
  /// A `TextEdit` inserting `import <import_path>` right after the last
511
528
  /// existing `import` line (or after the `module` line, or at the very top of
512
529
  /// the file if neither exists) — plain line scanning, since `ast::Import`
plum-lsp/src/main.rs CHANGED
@@ -531,9 +531,18 @@ mod tests {
531
531
  ("Testing", "std/Testing"),
532
532
  ("Http", "std/Http"),
533
533
  ("Int", "std/Int"),
534
+ ("Stage", "std/game/Scene"),
535
+ ("drawRect", "std/game/Game"),
536
+ ("Vec2", "std/game/Vec2"),
534
537
  ] {
535
538
  assert_eq!(index.get(name).map(String::as_str), Some(path), "{name}");
536
539
  }
540
+ // Completing a game name offers `std/game/Game` and auto-imports that path.
541
+ let edit = completion::importInsertionEdit(
542
+ "module t\nimport std/Int\n",
543
+ index.get("drawRect").expect("drawRect indexed"),
544
+ );
545
+ assert_eq!(edit.new_text, "import std/game/Game\n");
537
546
  let dir = std::env::temp_dir().join(format!("plum-lsp-ns-{}", std::process::id()));
538
547
  std::fs::create_dir_all(&dir).unwrap();
539
548
  let path = dir.join("t.plum");
plum-std/{Color.plum → game/Color.plum} RENAMED
File without changes
plum-std/{Game.plum → game/Game.plum} RENAMED
@@ -2,8 +2,8 @@ module std
2
2
  import std/Int
3
3
  import std/Float
4
4
  import std/Str
5
- import std/Vec2
5
+ import std/game/Vec2
6
- import std/Color
6
+ import std/game/Color
7
7
 
8
8
  # The host API of a game: drawing, textures, sounds, files and input, all
9
9
  # provided by the game host (`plum game`). Texture and sound handles are -1 on
plum-std/{Scene.plum → game/Scene.plum} RENAMED
@@ -4,9 +4,9 @@ import std/Float
4
4
  import std/Str
5
5
  import std/List
6
6
  import std/Math
7
- import std/Vec2
7
+ import std/game/Vec2
8
- import std/Color
8
+ import std/game/Color
9
- import std/Game
9
+ import std/game/Game
10
10
 
11
11
  # A Scene2D-like scene graph: a tree of `Node`s under a `Stage`. A node has a
12
12
  # position, size, origin (the pivot of its rotation and scale), scale, rotation
@@ -20,7 +20,7 @@ import std/Game
20
20
  # stage.root.add(ring)
21
21
  # ring.add(Node.rect(40.0, 20.0, Color.Red).at(10.0, 5.0))
22
22
  # stage.act(dt) # runs every node's `act` behaviour, depth first
23
- # stage.draw() # draws through `std/Game`
23
+ # stage.draw() # draws through `std/game/Game`
24
24
  #
25
25
  # The transform maths (`Affine`, `localMatrix`, `worldMatrix`, `flatten`,
26
26
  # `hitNode`) never calls the host, so it runs under `plum test`.
plum-std/{Vec2.plum → game/Vec2.plum} RENAMED
File without changes