@tishlang/tish 1.6.0 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/Cargo.toml +2 -0
  2. package/README.md +2 -0
  3. package/bin/tish +0 -0
  4. package/crates/js_to_tish/src/error.rs +2 -8
  5. package/crates/js_to_tish/src/transform/expr.rs +128 -137
  6. package/crates/js_to_tish/src/transform/stmt.rs +62 -32
  7. package/crates/tish/Cargo.toml +15 -5
  8. package/crates/tish/src/cargo_native_registry.rs +29 -0
  9. package/crates/tish/src/cli_help.rs +92 -39
  10. package/crates/tish/src/main.rs +172 -86
  11. package/crates/tish/src/repl_completion.rs +3 -3
  12. package/crates/tish/tests/cargo_example_compile.rs +4 -2
  13. package/crates/tish/tests/integration_test.rs +216 -54
  14. package/crates/tish/tests/run_optimize_stdout_parity.rs +3 -7
  15. package/crates/tish/tests/shortcircuit.rs +20 -5
  16. package/crates/tish_ast/src/ast.rs +92 -23
  17. package/crates/tish_build_utils/Cargo.toml +4 -0
  18. package/crates/tish_build_utils/src/lib.rs +136 -8
  19. package/crates/tish_builtins/Cargo.toml +5 -1
  20. package/crates/tish_builtins/src/array.rs +65 -33
  21. package/crates/tish_builtins/src/construct.rs +34 -39
  22. package/crates/tish_builtins/src/globals.rs +42 -26
  23. package/crates/tish_builtins/src/helpers.rs +2 -1
  24. package/crates/tish_builtins/src/lib.rs +5 -5
  25. package/crates/tish_builtins/src/math.rs +5 -3
  26. package/crates/tish_builtins/src/object.rs +3 -2
  27. package/crates/tish_builtins/src/string.rs +144 -22
  28. package/crates/tish_bytecode/src/chunk.rs +0 -1
  29. package/crates/tish_bytecode/src/compiler.rs +173 -71
  30. package/crates/tish_bytecode/src/opcode.rs +24 -6
  31. package/crates/tish_bytecode/src/peephole.rs +2 -2
  32. package/crates/tish_compile/Cargo.toml +1 -0
  33. package/crates/tish_compile/src/codegen.rs +1621 -453
  34. package/crates/tish_compile/src/infer.rs +75 -19
  35. package/crates/tish_compile/src/lib.rs +19 -8
  36. package/crates/tish_compile/src/resolve.rs +278 -137
  37. package/crates/tish_compile/src/types.rs +184 -24
  38. package/crates/tish_compile_js/Cargo.toml +1 -0
  39. package/crates/tish_compile_js/src/codegen.rs +181 -37
  40. package/crates/tish_compile_js/src/lib.rs +3 -1
  41. package/crates/tish_compile_js/src/tests_jsx.rs +30 -6
  42. package/crates/tish_compiler_wasm/src/lib.rs +16 -13
  43. package/crates/tish_compiler_wasm/src/resolve_virtual.rs +69 -59
  44. package/crates/tish_core/Cargo.toml +8 -0
  45. package/crates/tish_core/src/json.rs +107 -56
  46. package/crates/tish_core/src/lib.rs +4 -2
  47. package/crates/tish_core/src/macros.rs +5 -5
  48. package/crates/tish_core/src/uri.rs +9 -6
  49. package/crates/tish_core/src/value.rs +145 -43
  50. package/crates/tish_core/src/vmref.rs +178 -0
  51. package/crates/tish_cranelift/src/link.rs +6 -9
  52. package/crates/tish_cranelift/src/lower.rs +14 -8
  53. package/crates/tish_eval/Cargo.toml +17 -2
  54. package/crates/tish_eval/src/eval.rs +474 -165
  55. package/crates/tish_eval/src/http.rs +61 -0
  56. package/crates/tish_eval/src/lib.rs +12 -8
  57. package/crates/tish_eval/src/natives.rs +136 -38
  58. package/crates/tish_eval/src/promise.rs +14 -8
  59. package/crates/tish_eval/src/timers.rs +28 -19
  60. package/crates/tish_eval/src/value.rs +17 -6
  61. package/crates/tish_eval/src/value_convert.rs +13 -5
  62. package/crates/tish_fmt/src/lib.rs +149 -43
  63. package/crates/tish_lexer/src/lib.rs +232 -63
  64. package/crates/tish_lexer/src/token.rs +10 -6
  65. package/crates/tish_llvm/src/lib.rs +17 -8
  66. package/crates/tish_lsp/Cargo.toml +4 -1
  67. package/crates/tish_lsp/README.md +1 -1
  68. package/crates/tish_lsp/src/builtin_goto.rs +261 -0
  69. package/crates/tish_lsp/src/import_goto.rs +549 -0
  70. package/crates/tish_lsp/src/main.rs +504 -106
  71. package/crates/tish_native/src/build.rs +4 -8
  72. package/crates/tish_native/src/lib.rs +54 -21
  73. package/crates/tish_opt/src/lib.rs +84 -52
  74. package/crates/tish_parser/src/lib.rs +45 -13
  75. package/crates/tish_parser/src/parser.rs +505 -130
  76. package/crates/tish_resolve/Cargo.toml +13 -0
  77. package/crates/tish_resolve/src/lib.rs +3436 -0
  78. package/crates/tish_resolve/src/pos.rs +133 -0
  79. package/crates/tish_runtime/Cargo.toml +68 -3
  80. package/crates/tish_runtime/src/http.rs +1136 -145
  81. package/crates/tish_runtime/src/http_fetch.rs +38 -27
  82. package/crates/tish_runtime/src/http_hyper.rs +418 -0
  83. package/crates/tish_runtime/src/http_prefork.rs +189 -0
  84. package/crates/tish_runtime/src/lib.rs +375 -189
  85. package/crates/tish_runtime/src/promise.rs +199 -40
  86. package/crates/tish_runtime/src/promise_io.rs +2 -1
  87. package/crates/tish_runtime/src/timers.rs +37 -1
  88. package/crates/tish_runtime/src/ws.rs +65 -42
  89. package/crates/tish_runtime/tests/fetch_readable_stream.rs +5 -4
  90. package/crates/tish_ui/src/jsx.rs +317 -27
  91. package/crates/tish_ui/src/lib.rs +5 -2
  92. package/crates/tish_ui/src/runtime/hooks.rs +406 -45
  93. package/crates/tish_ui/src/runtime/mod.rs +36 -9
  94. package/crates/tish_vm/Cargo.toml +15 -5
  95. package/crates/tish_vm/src/vm.rs +725 -281
  96. package/crates/tish_vm/tests/peephole_jump_chain_logical_or.rs +11 -4
  97. package/crates/tish_wasm/src/lib.rs +55 -42
  98. package/crates/tish_wasm_runtime/Cargo.toml +2 -1
  99. package/crates/tish_wasm_runtime/src/lib.rs +1 -1
  100. package/crates/tishlang_cargo_bindgen/Cargo.toml +26 -0
  101. package/crates/tishlang_cargo_bindgen/src/classify.rs +265 -0
  102. package/crates/tishlang_cargo_bindgen/src/discover.rs +120 -0
  103. package/crates/tishlang_cargo_bindgen/src/infer.rs +372 -0
  104. package/crates/tishlang_cargo_bindgen/src/lib.rs +350 -0
  105. package/crates/tishlang_cargo_bindgen/src/main.rs +164 -0
  106. package/crates/tishlang_cargo_bindgen/src/metadata.rs +114 -0
  107. package/justfile +8 -0
  108. package/package.json +1 -1
  109. package/platform/darwin-arm64/tish +0 -0
  110. package/platform/darwin-x64/tish +0 -0
  111. package/platform/linux-arm64/tish +0 -0
  112. package/platform/linux-x64/tish +0 -0
  113. package/platform/win32-x64/tish.exe +0 -0
@@ -0,0 +1,549 @@
1
+ //! Go to definition for import specifiers: relative paths, bare `node_modules` packages, and
2
+ //! native `tish:` / `@scope/pkg` / `cargo:` → Rust `pub fn` sites (via `syn` + optional `cargo metadata`).
3
+
4
+ use std::collections::HashMap;
5
+ use std::path::{Path, PathBuf};
6
+ use std::sync::{Arc, RwLock};
7
+
8
+ use regex::Regex;
9
+ use tower_lsp::lsp_types::{Location, Position, Range, Url};
10
+
11
+ use tishlang_ast::{ImportSpecifier, Program, Statement};
12
+ use tishlang_resolve::member_access_chain_at_cursor;
13
+ use tishlang_compile::{
14
+ export_name_to_rust_ident, is_builtin_native_spec, is_cargo_native_spec, is_native_import,
15
+ normalize_builtin_spec, read_project_tish_config, resolve_bare_spec, resolve_native_modules,
16
+ };
17
+
18
+ /// Pick a workspace / project root for resolving `package.json` and `node_modules`.
19
+ fn infer_project_root(file_path: &Path, roots: &[PathBuf]) -> Option<PathBuf> {
20
+ if let Ok(can) = file_path.canonicalize() {
21
+ for r in roots {
22
+ if let Ok(rc) = r.canonicalize() {
23
+ if can.starts_with(&rc) {
24
+ return Some(rc);
25
+ }
26
+ }
27
+ }
28
+ }
29
+ let mut dir = file_path.parent()?.to_path_buf();
30
+ loop {
31
+ if dir.join("package.json").exists() || dir.join("Cargo.toml").exists() {
32
+ return dir.canonicalize().ok().or(Some(dir));
33
+ }
34
+ dir = dir.parent()?.to_path_buf();
35
+ }
36
+ }
37
+
38
+ fn location_from_rust_path(path: &Path, line: u32, col: u32) -> Option<Location> {
39
+ let uri = Url::from_file_path(path).ok()?;
40
+ let line_str = std::fs::read_to_string(path).ok()?;
41
+ let line_slice = line_str.lines().nth(line as usize).unwrap_or("");
42
+ let end_char = line_slice.len() as u32;
43
+ Some(Location {
44
+ uri,
45
+ range: Range {
46
+ start: Position { line, character: col },
47
+ end: Position {
48
+ line,
49
+ character: end_char.max(col.saturating_add(1)),
50
+ },
51
+ },
52
+ })
53
+ }
54
+
55
+ fn resolve_cargo_dep_crate_root(
56
+ project_root: &Path,
57
+ dep_key: &str,
58
+ raw: &serde_json::Value,
59
+ ) -> Result<PathBuf, String> {
60
+ match raw {
61
+ serde_json::Value::String(ver) => {
62
+ let r = tishlang_cargo_bindgen::resolve_registry_dependency(dep_key, ver)?;
63
+ Ok(r.source_root())
64
+ }
65
+ serde_json::Value::Object(map) => {
66
+ let path_str = map
67
+ .get("path")
68
+ .and_then(|v| v.as_str())
69
+ .ok_or_else(|| {
70
+ "cargo:… rustDependencies entry must be a version string or an object with \"path\""
71
+ .to_string()
72
+ })?;
73
+ let glue = Path::new(path_str);
74
+ let glue = if glue.is_absolute() {
75
+ glue.to_path_buf()
76
+ } else {
77
+ project_root.join(path_str)
78
+ };
79
+ let glue = glue.canonicalize().unwrap_or(glue);
80
+ let glue_cargo = glue.join("Cargo.toml");
81
+ if glue_cargo.is_file() {
82
+ match tishlang_cargo_bindgen::resolve_dependency_from_manifest(&glue_cargo, dep_key) {
83
+ Ok(r) => Ok(r.source_root()),
84
+ Err(_) if glue.join("src").is_dir() => Ok(glue),
85
+ Err(e) => Err(e),
86
+ }
87
+ } else if glue.join("src").is_dir() {
88
+ Ok(glue)
89
+ } else {
90
+ Err(format!(
91
+ "path dependency for {} does not look like a Rust crate: {}",
92
+ dep_key,
93
+ glue.display()
94
+ ))
95
+ }
96
+ }
97
+ _ => Err(format!(
98
+ "tish.rustDependencies.{} must be a string (semver) or object with path",
99
+ dep_key
100
+ )),
101
+ }
102
+ }
103
+
104
+ fn cargo_crate_root_cached(
105
+ project_root: &Path,
106
+ spec: &str,
107
+ dep_key: &str,
108
+ raw: &serde_json::Value,
109
+ cache: &RwLock<HashMap<(PathBuf, String), PathBuf>>,
110
+ ) -> Result<PathBuf, String> {
111
+ let key = (project_root.to_path_buf(), spec.to_string());
112
+ if let Ok(g) = cache.read() {
113
+ if let Some(p) = g.get(&key) {
114
+ return Ok(p.clone());
115
+ }
116
+ }
117
+ let root = resolve_cargo_dep_crate_root(project_root, dep_key, raw)?;
118
+ if let Ok(mut g) = cache.write() {
119
+ g.insert(key, root.clone());
120
+ }
121
+ Ok(root)
122
+ }
123
+
124
+ fn rust_def_for_crate_root(crate_root: &Path, tish_export: &str) -> Option<Location> {
125
+ let snake = export_name_to_rust_ident(tish_export);
126
+ let try_names: [&str; 2] = [&snake, tish_export];
127
+ for name in try_names {
128
+ if name.is_empty() {
129
+ continue;
130
+ }
131
+ if let Ok((path, line, col)) = tishlang_cargo_bindgen::rust_public_fn_location(crate_root, name)
132
+ {
133
+ return location_from_rust_path(&path, line, col);
134
+ }
135
+ }
136
+ None
137
+ }
138
+
139
+ fn rust_fn_location_exact(crate_root: &Path, rust_fn_ident: &str) -> Option<Location> {
140
+ if rust_fn_ident.is_empty() {
141
+ return None;
142
+ }
143
+ let (path, line, col) =
144
+ tishlang_cargo_bindgen::rust_public_fn_location(crate_root, rust_fn_ident).ok()?;
145
+ location_from_rust_path(&path, line, col)
146
+ }
147
+
148
+ fn is_ident_char_member(c: char) -> bool {
149
+ c.is_alphanumeric() || c == '_'
150
+ }
151
+
152
+ /// When the cursor is on `member` in `receiver.member` on a single line, returns `(receiver, member)`.
153
+ fn split_receiver_member(line: &str, col: usize) -> Option<(String, String)> {
154
+ let chars: Vec<char> = line.chars().collect();
155
+ if chars.is_empty() {
156
+ return None;
157
+ }
158
+ let col = col.min(chars.len().saturating_sub(1));
159
+ if !is_ident_char_member(chars[col]) {
160
+ return None;
161
+ }
162
+ let mut end = col;
163
+ while end + 1 < chars.len() && is_ident_char_member(chars[end + 1]) {
164
+ end += 1;
165
+ }
166
+ let mut start = col;
167
+ while start > 0 && is_ident_char_member(chars[start - 1]) {
168
+ start -= 1;
169
+ }
170
+ if start == 0 {
171
+ return None;
172
+ }
173
+ if chars[start - 1] != '.' {
174
+ return None;
175
+ }
176
+ let member: String = chars[start..=end].iter().collect();
177
+ let mut k = start - 2;
178
+ while k > 0 && is_ident_char_member(chars[k - 1]) {
179
+ k -= 1;
180
+ }
181
+ let receiver: String = chars[k..start - 1].iter().collect();
182
+ if receiver.is_empty() {
183
+ return None;
184
+ }
185
+ Some((receiver, member))
186
+ }
187
+
188
+ fn try_rust_member_on_crate(
189
+ crate_root: &Path,
190
+ members: &[Arc<str>],
191
+ imported_receiver: Option<&str>,
192
+ ) -> Option<Location> {
193
+ let last = members.last()?.as_ref();
194
+ if let Some(loc) = rust_def_for_crate_root(crate_root, last) {
195
+ return Some(loc);
196
+ }
197
+ if members.len() >= 2 {
198
+ let joined = members
199
+ .iter()
200
+ .map(|m| export_name_to_rust_ident(m.as_ref()))
201
+ .collect::<Vec<_>>()
202
+ .join("_");
203
+ if let Some(loc) = rust_fn_location_exact(crate_root, &joined) {
204
+ return Some(loc);
205
+ }
206
+ }
207
+ if let Some(im) = imported_receiver {
208
+ let mut s = export_name_to_rust_ident(im);
209
+ for m in members {
210
+ s.push('_');
211
+ s.push_str(&export_name_to_rust_ident(m.as_ref()));
212
+ }
213
+ if let Some(loc) = rust_fn_location_exact(crate_root, &s) {
214
+ return Some(loc);
215
+ }
216
+ }
217
+ None
218
+ }
219
+
220
+ /// Optional `| …` hover line after the 1-based line in a native package’s `lsp-pragmas.d.tish`.
221
+ fn parse_lsp_pragmas_native(src: &str) -> HashMap<String, (crate::builtin_goto::BuiltinDef, Option<String>)> {
222
+ let re = Regex::new(
223
+ r"(?m)^\s*//\s*@tish-source\s+(\S+)\s+(\S+)\s+(\d+)(?:\s*\|\s*(.*?))?\s*$",
224
+ )
225
+ .expect("native lsp pragma regex");
226
+ let mut m = HashMap::new();
227
+ for cap in re.captures_iter(src) {
228
+ let sym = cap[1].to_string();
229
+ let rel = cap[2].to_string();
230
+ let line_1: u32 = cap[3].parse().unwrap_or(1);
231
+ let doc = cap
232
+ .get(4)
233
+ .map(|g| g.as_str().trim().to_string())
234
+ .filter(|s| !s.is_empty());
235
+ m.insert(
236
+ sym,
237
+ (
238
+ crate::builtin_goto::BuiltinDef {
239
+ rel_path: rel,
240
+ line: line_1.saturating_sub(1),
241
+ character: 0,
242
+ },
243
+ doc,
244
+ ),
245
+ );
246
+ }
247
+ m
248
+ }
249
+
250
+ fn pragma_key_for_native_member(
251
+ imported_for_prefix: Option<&str>,
252
+ members: &[Arc<str>],
253
+ ) -> Option<String> {
254
+ if members.is_empty() {
255
+ return None;
256
+ }
257
+ match imported_for_prefix {
258
+ Some(prefix) => {
259
+ let tail: String = members.iter().map(|m| m.as_ref()).collect::<Vec<_>>().join(".");
260
+ if tail.is_empty() {
261
+ None
262
+ } else {
263
+ Some(format!("{prefix}.{tail}"))
264
+ }
265
+ }
266
+ None => {
267
+ if members.len() >= 2 {
268
+ Some(
269
+ members
270
+ .iter()
271
+ .map(|m| m.as_ref())
272
+ .collect::<Vec<_>>()
273
+ .join("."),
274
+ )
275
+ } else {
276
+ None
277
+ }
278
+ }
279
+ }
280
+ }
281
+
282
+ fn lookup_lsp_pragma_in_crate_root(
283
+ crate_root: &Path,
284
+ key: &str,
285
+ ) -> Option<(crate::builtin_goto::BuiltinDef, Option<String>)> {
286
+ let path = crate_root.join("lsp-pragmas.d.tish");
287
+ let src = std::fs::read_to_string(&path).ok()?;
288
+ let map = parse_lsp_pragmas_native(&src);
289
+ map.get(key).cloned()
290
+ }
291
+
292
+ #[derive(Debug, Clone)]
293
+ pub struct NativeMemberDefinition {
294
+ pub location: Location,
295
+ /// From `lsp-pragmas.d.tish` when Rust `pub fn` resolution misses.
296
+ pub doc: Option<String>,
297
+ }
298
+
299
+ /// Static member chain `root.a.b` where `root` is an import: resolve the leaf to a Rust `pub fn`,
300
+ /// else to `lsp-pragmas.d.tish` in the native package (e.g. `tish-macos`).
301
+ pub fn native_member_definition(
302
+ program: &Program,
303
+ file_path: &Path,
304
+ text: &str,
305
+ roots: &[PathBuf],
306
+ cargo_src_cache: &RwLock<HashMap<(PathBuf, String), PathBuf>>,
307
+ lsp_line: u32,
308
+ lsp_character: u32,
309
+ word: &str,
310
+ ) -> Option<NativeMemberDefinition> {
311
+ let project_root = infer_project_root(file_path, roots)?;
312
+ let from_dir = file_path.parent()?;
313
+
314
+ let (receiver_local, members): (String, Vec<Arc<str>>) =
315
+ if let Some(ch) = member_access_chain_at_cursor(program, text, lsp_line, lsp_character) {
316
+ if ch.members.last()?.as_ref() != word {
317
+ return None;
318
+ }
319
+ (ch.root_local.as_ref().to_string(), ch.members)
320
+ } else {
321
+ let line_str = text.lines().nth(lsp_line as usize)?;
322
+ let col = lsp_character as usize;
323
+ let (recv, member) = split_receiver_member(line_str, col)?;
324
+ if member != word {
325
+ return None;
326
+ }
327
+ (recv, vec![Arc::from(member.as_str())])
328
+ };
329
+
330
+ for stmt in &program.statements {
331
+ let Statement::Import {
332
+ specifiers, from, ..
333
+ } = stmt
334
+ else {
335
+ continue;
336
+ };
337
+ let from_s = from.as_ref();
338
+ for sp in specifiers {
339
+ let (imported_for_prefix, local) = match sp {
340
+ ImportSpecifier::Named { name, alias, .. } => (
341
+ Some(name.as_ref()),
342
+ alias.as_ref().map(|a| a.as_ref()).unwrap_or(name.as_ref()),
343
+ ),
344
+ ImportSpecifier::Default { name, .. } => (Some(name.as_ref()), name.as_ref()),
345
+ ImportSpecifier::Namespace { name, .. } => (None, name.as_ref()),
346
+ };
347
+ if local != receiver_local.as_str() {
348
+ continue;
349
+ }
350
+
351
+ if from_s.starts_with("./") || from_s.starts_with("../") {
352
+ if members.len() == 1 {
353
+ let loc = resolve_relative_tish(from_dir, from_s, members[0].as_ref())?;
354
+ return Some(NativeMemberDefinition {
355
+ location: loc,
356
+ doc: None,
357
+ });
358
+ }
359
+ continue;
360
+ }
361
+
362
+ if !is_native_import(from_s) {
363
+ continue;
364
+ }
365
+
366
+ let spec = normalize_builtin_spec(from_s).unwrap_or_else(|| from_s.to_string());
367
+ if is_builtin_native_spec(&spec) {
368
+ continue;
369
+ }
370
+
371
+ let crate_root = if is_cargo_native_spec(&spec) {
372
+ let dep_key = spec.strip_prefix("cargo:")?;
373
+ let tish = read_project_tish_config(&project_root);
374
+ let raw = tish
375
+ .get("rustDependencies")
376
+ .and_then(|v| v.get(dep_key))
377
+ .cloned()?;
378
+ cargo_crate_root_cached(&project_root, &spec, dep_key, &raw, cargo_src_cache).ok()?
379
+ } else {
380
+ let mods = resolve_native_modules(program, &project_root).ok()?;
381
+ let m = mods.iter().find(|mm| mm.spec == spec)?;
382
+ m.crate_path.clone()
383
+ };
384
+
385
+ if let Some(loc) = try_rust_member_on_crate(&crate_root, &members, imported_for_prefix) {
386
+ return Some(NativeMemberDefinition {
387
+ location: loc,
388
+ doc: None,
389
+ });
390
+ }
391
+ if let Some(key) = pragma_key_for_native_member(imported_for_prefix, &members) {
392
+ if let Some((def, doc)) = lookup_lsp_pragma_in_crate_root(&crate_root, &key) {
393
+ if let Some(loc) = crate::builtin_goto::to_file_location(&crate_root, &def) {
394
+ return Some(NativeMemberDefinition { location: loc, doc });
395
+ }
396
+ }
397
+ }
398
+ return None;
399
+ }
400
+ }
401
+ None
402
+ }
403
+
404
+ /// Static member chain `root.a.b` where `root` is an import: resolve the leaf name to a Rust `pub fn`
405
+ /// (native / `cargo:`) or a single-level export in a relative `.tish` module.
406
+ pub fn definition_for_native_receiver_member(
407
+ program: &Program,
408
+ file_path: &Path,
409
+ text: &str,
410
+ roots: &[PathBuf],
411
+ cargo_src_cache: &RwLock<HashMap<(PathBuf, String), PathBuf>>,
412
+ lsp_line: u32,
413
+ lsp_character: u32,
414
+ word: &str,
415
+ ) -> Option<Location> {
416
+ native_member_definition(
417
+ program,
418
+ file_path,
419
+ text,
420
+ roots,
421
+ cargo_src_cache,
422
+ lsp_line,
423
+ lsp_character,
424
+ word,
425
+ )
426
+ .map(|d| d.location)
427
+ }
428
+
429
+ fn resolve_relative_tish(from_dir: &Path, from_s: &str, imported: &str) -> Option<Location> {
430
+ let target = from_dir.join(from_s.trim_start_matches("./"));
431
+ let target = if target.extension().is_none() {
432
+ target.with_extension("tish")
433
+ } else {
434
+ target
435
+ };
436
+ let can = target.canonicalize().ok()?;
437
+ let u = Url::from_file_path(&can).ok()?;
438
+ let src = std::fs::read_to_string(&can).ok()?;
439
+ let prog = tishlang_parser::parse(&src).ok()?;
440
+ crate::find_export(&prog, imported, &u, &src)
441
+ }
442
+
443
+ /// After same-file [`tishlang_resolve::definition_span`] misses, resolve import sites (Tish files,
444
+ /// `node_modules` packages, and Rust `pub fn` for native / `cargo:` imports).
445
+ pub fn definition_for_import(
446
+ program: &Program,
447
+ file_path: &Path,
448
+ word: &str,
449
+ roots: &[PathBuf],
450
+ cargo_src_cache: &RwLock<HashMap<(PathBuf, String), PathBuf>>,
451
+ ) -> Option<Location> {
452
+ if word.is_empty() {
453
+ return None;
454
+ }
455
+ let project_root = infer_project_root(file_path, roots)?;
456
+ let from_dir = file_path.parent()?;
457
+
458
+ for stmt in &program.statements {
459
+ let Statement::Import {
460
+ specifiers, from, ..
461
+ } = stmt
462
+ else {
463
+ continue;
464
+ };
465
+ let from_s = from.as_ref();
466
+ for sp in specifiers {
467
+ let (imported, local) = match sp {
468
+ ImportSpecifier::Named { name, alias, .. } => (
469
+ name.as_ref(),
470
+ alias.as_ref().map(|a| a.as_ref()).unwrap_or(name.as_ref()),
471
+ ),
472
+ ImportSpecifier::Default { name, .. } => (name.as_ref(), name.as_ref()),
473
+ ImportSpecifier::Namespace { .. } => continue,
474
+ };
475
+ if local != word {
476
+ continue;
477
+ }
478
+
479
+ if from_s.starts_with("./") || from_s.starts_with("../") {
480
+ return resolve_relative_tish(from_dir, from_s, imported);
481
+ }
482
+
483
+ if is_native_import(from_s) {
484
+ let spec = normalize_builtin_spec(from_s).unwrap_or_else(|| from_s.to_string());
485
+ if is_builtin_native_spec(&spec) {
486
+ return None;
487
+ }
488
+ if is_cargo_native_spec(&spec) {
489
+ let dep_key = spec.strip_prefix("cargo:")?;
490
+ let tish = read_project_tish_config(&project_root);
491
+ let raw = tish
492
+ .get("rustDependencies")
493
+ .and_then(|v| v.get(dep_key))
494
+ .cloned()?;
495
+ let crate_root =
496
+ cargo_crate_root_cached(&project_root, &spec, dep_key, &raw, cargo_src_cache)
497
+ .ok()?;
498
+ return rust_def_for_crate_root(&crate_root, imported);
499
+ }
500
+
501
+ let mods = resolve_native_modules(program, &project_root).ok()?;
502
+ let m = mods.iter().find(|mm| mm.spec == spec)?;
503
+ return rust_def_for_crate_root(&m.crate_path, imported);
504
+ }
505
+
506
+ let entry = resolve_bare_spec(from_s, from_dir, &project_root)?;
507
+ let u = Url::from_file_path(&entry).ok()?;
508
+ let src = std::fs::read_to_string(&entry).ok()?;
509
+ let prog = tishlang_parser::parse(&src).ok()?;
510
+ return crate::find_export(&prog, imported, &u, &src);
511
+ }
512
+ }
513
+ None
514
+ }
515
+
516
+ #[cfg(test)]
517
+ mod receiver_member_tests {
518
+ use std::sync::Arc;
519
+
520
+ #[test]
521
+ fn splits_window_set_title() {
522
+ let line = "window.setTitle(\"Hi\")";
523
+ let col = "window.".len(); // on 's'
524
+ let (recv, mem) = super::split_receiver_member(line, col).expect("split");
525
+ assert_eq!(recv, "window");
526
+ assert_eq!(mem, "setTitle");
527
+ }
528
+
529
+ #[test]
530
+ fn native_pragma_parse_optional_doc() {
531
+ let src = r"// @tish-source window.innerHeight src/appkit/window_api.rs 289 | Height in points.
532
+ // @tish-source window.innerWidth src/appkit/window_api.rs 272
533
+ ";
534
+ let m = super::parse_lsp_pragmas_native(src);
535
+ let (def, doc) = m.get("window.innerHeight").expect("innerHeight");
536
+ assert_eq!(def.rel_path, "src/appkit/window_api.rs");
537
+ assert_eq!(def.line, 288);
538
+ assert_eq!(doc.as_deref(), Some("Height in points."));
539
+ let (def2, doc2) = m.get("window.innerWidth").expect("innerWidth");
540
+ assert_eq!(def2.line, 271);
541
+ assert!(doc2.is_none());
542
+ }
543
+
544
+ #[test]
545
+ fn pragma_key_named_import() {
546
+ let k = super::pragma_key_for_native_member(Some("window"), &[Arc::from("innerHeight")]);
547
+ assert_eq!(k.as_deref(), Some("window.innerHeight"));
548
+ }
549
+ }