@tishlang/tish-format 1.0.12 → 2.0.1

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 (189) hide show
  1. package/Cargo.toml +51 -0
  2. package/LICENSE +13 -0
  3. package/bin/tish-format +0 -0
  4. package/crates/js_to_tish/Cargo.toml +11 -0
  5. package/crates/js_to_tish/README.md +18 -0
  6. package/crates/js_to_tish/src/error.rs +55 -0
  7. package/crates/js_to_tish/src/lib.rs +11 -0
  8. package/crates/js_to_tish/src/span_util.rs +35 -0
  9. package/crates/js_to_tish/src/transform/expr.rs +611 -0
  10. package/crates/js_to_tish/src/transform/stmt.rs +503 -0
  11. package/crates/js_to_tish/src/transform.rs +60 -0
  12. package/crates/tish/Cargo.toml +62 -0
  13. package/crates/tish/build.rs +21 -0
  14. package/crates/tish/src/cargo_native_registry.rs +32 -0
  15. package/crates/tish/src/cli_help.rs +576 -0
  16. package/crates/tish/src/main.rs +853 -0
  17. package/crates/tish/src/repl_completion.rs +199 -0
  18. package/crates/tish/tests/cargo_example_compile.rs +67 -0
  19. package/crates/tish/tests/error_source_location.rs +36 -0
  20. package/crates/tish/tests/fixtures/cargo_example_project/Cargo.toml +3 -0
  21. package/crates/tish/tests/fixtures/cargo_example_project/crates/demo-shim/Cargo.toml +11 -0
  22. package/crates/tish/tests/fixtures/cargo_example_project/crates/demo-shim/src/lib.rs +12 -0
  23. package/crates/tish/tests/fixtures/cargo_example_project/package.json +10 -0
  24. package/crates/tish/tests/fixtures/cargo_example_project/src/main.tish +3 -0
  25. package/crates/tish/tests/fixtures/runtime_error_location.tish +5 -0
  26. package/crates/tish/tests/fixtures/trycatch_runtime_errors.tish +15 -0
  27. package/crates/tish/tests/fixtures/tty_capability.tish +9 -0
  28. package/crates/tish/tests/integration_test.rs +1406 -0
  29. package/crates/tish/tests/run_optimize_stdout_parity.rs +50 -0
  30. package/crates/tish/tests/shortcircuit.rs +65 -0
  31. package/crates/tish/tests/trycatch_runtime_errors.rs +45 -0
  32. package/crates/tish/tests/tty_capability.rs +43 -0
  33. package/crates/tish_ast/Cargo.toml +9 -0
  34. package/crates/tish_ast/src/ast.rs +649 -0
  35. package/crates/tish_ast/src/lib.rs +5 -0
  36. package/crates/tish_build_utils/Cargo.toml +11 -0
  37. package/crates/tish_build_utils/src/lib.rs +577 -0
  38. package/crates/tish_builtins/Cargo.toml +22 -0
  39. package/crates/tish_builtins/src/array.rs +803 -0
  40. package/crates/tish_builtins/src/collections.rs +481 -0
  41. package/crates/tish_builtins/src/construct.rs +199 -0
  42. package/crates/tish_builtins/src/date.rs +538 -0
  43. package/crates/tish_builtins/src/globals.rs +293 -0
  44. package/crates/tish_builtins/src/helpers.rs +35 -0
  45. package/crates/tish_builtins/src/iterator.rs +129 -0
  46. package/crates/tish_builtins/src/lib.rs +21 -0
  47. package/crates/tish_builtins/src/math.rs +89 -0
  48. package/crates/tish_builtins/src/number.rs +96 -0
  49. package/crates/tish_builtins/src/object.rs +36 -0
  50. package/crates/tish_builtins/src/string.rs +646 -0
  51. package/crates/tish_builtins/src/symbol.rs +83 -0
  52. package/crates/tish_builtins/src/typedarrays.rs +298 -0
  53. package/crates/tish_bytecode/Cargo.toml +17 -0
  54. package/crates/tish_bytecode/src/chunk.rs +164 -0
  55. package/crates/tish_bytecode/src/compiler.rs +2604 -0
  56. package/crates/tish_bytecode/src/encoding.rs +102 -0
  57. package/crates/tish_bytecode/src/lib.rs +20 -0
  58. package/crates/tish_bytecode/src/opcode.rs +185 -0
  59. package/crates/tish_bytecode/src/peephole.rs +189 -0
  60. package/crates/tish_bytecode/src/serialize.rs +193 -0
  61. package/crates/tish_bytecode/tests/break_continue_bytecode.rs +44 -0
  62. package/crates/tish_bytecode/tests/constant_folding.rs +84 -0
  63. package/crates/tish_bytecode/tests/sort_optimization.rs +31 -0
  64. package/crates/tish_compile/Cargo.toml +27 -0
  65. package/crates/tish_compile/src/check.rs +774 -0
  66. package/crates/tish_compile/src/codegen.rs +7317 -0
  67. package/crates/tish_compile/src/infer.rs +1681 -0
  68. package/crates/tish_compile/src/lib.rs +206 -0
  69. package/crates/tish_compile/src/resolve.rs +1951 -0
  70. package/crates/tish_compile/src/types.rs +605 -0
  71. package/crates/tish_compile_js/Cargo.toml +18 -0
  72. package/crates/tish_compile_js/examples/jsx_vdom_smoke.tish +8 -0
  73. package/crates/tish_compile_js/src/codegen.rs +938 -0
  74. package/crates/tish_compile_js/src/error.rs +20 -0
  75. package/crates/tish_compile_js/src/lib.rs +26 -0
  76. package/crates/tish_compile_js/src/tests_jsx.rs +414 -0
  77. package/crates/tish_compiler_wasm/Cargo.toml +21 -0
  78. package/crates/tish_compiler_wasm/src/lib.rs +57 -0
  79. package/crates/tish_compiler_wasm/src/resolve_virtual.rs +473 -0
  80. package/crates/tish_core/Cargo.toml +32 -0
  81. package/crates/tish_core/src/console_style.rs +170 -0
  82. package/crates/tish_core/src/json.rs +430 -0
  83. package/crates/tish_core/src/lib.rs +20 -0
  84. package/crates/tish_core/src/macros.rs +36 -0
  85. package/crates/tish_core/src/shape.rs +85 -0
  86. package/crates/tish_core/src/uri.rs +118 -0
  87. package/crates/tish_core/src/value.rs +1350 -0
  88. package/crates/tish_core/src/vmref.rs +183 -0
  89. package/crates/tish_cranelift/Cargo.toml +19 -0
  90. package/crates/tish_cranelift/src/lib.rs +43 -0
  91. package/crates/tish_cranelift/src/link.rs +130 -0
  92. package/crates/tish_cranelift/src/lower.rs +85 -0
  93. package/crates/tish_cranelift_runtime/Cargo.toml +26 -0
  94. package/crates/tish_cranelift_runtime/src/lib.rs +45 -0
  95. package/crates/tish_eval/Cargo.toml +51 -0
  96. package/crates/tish_eval/src/eval.rs +4265 -0
  97. package/crates/tish_eval/src/http.rs +191 -0
  98. package/crates/tish_eval/src/lib.rs +99 -0
  99. package/crates/tish_eval/src/natives.rs +551 -0
  100. package/crates/tish_eval/src/promise.rs +179 -0
  101. package/crates/tish_eval/src/regex.rs +299 -0
  102. package/crates/tish_eval/src/timers.rs +120 -0
  103. package/crates/tish_eval/src/value.rs +336 -0
  104. package/crates/tish_eval/src/value_convert.rs +117 -0
  105. package/crates/tish_ffi/Cargo.toml +26 -0
  106. package/crates/tish_ffi/src/lib.rs +518 -0
  107. package/crates/tish_ffi/tests/fixtures/testmod/Cargo.toml +18 -0
  108. package/crates/tish_ffi/tests/fixtures/testmod/src/lib.rs +46 -0
  109. package/crates/tish_ffi/tests/loader.rs +65 -0
  110. package/crates/tish_fmt/Cargo.toml +16 -0
  111. package/crates/tish_fmt/src/bin/tish-fmt.rs +41 -0
  112. package/crates/tish_fmt/src/lib.rs +2157 -0
  113. package/crates/tish_jsx_web/Cargo.toml +9 -0
  114. package/crates/tish_jsx_web/README.md +5 -0
  115. package/crates/tish_jsx_web/src/lib.rs +2 -0
  116. package/crates/tish_lexer/Cargo.toml +9 -0
  117. package/crates/tish_lexer/src/lib.rs +1104 -0
  118. package/crates/tish_lexer/src/token.rs +170 -0
  119. package/crates/tish_lint/Cargo.toml +18 -0
  120. package/crates/tish_lint/src/bin/tish-lint.rs +195 -0
  121. package/crates/tish_lint/src/lib.rs +281 -0
  122. package/crates/tish_llvm/Cargo.toml +13 -0
  123. package/crates/tish_llvm/src/lib.rs +115 -0
  124. package/crates/tish_lsp/Cargo.toml +25 -0
  125. package/crates/tish_lsp/README.md +26 -0
  126. package/crates/tish_lsp/src/builtin_goto.rs +362 -0
  127. package/crates/tish_lsp/src/import_goto.rs +564 -0
  128. package/crates/tish_lsp/src/main.rs +1459 -0
  129. package/crates/tish_native/Cargo.toml +16 -0
  130. package/crates/tish_native/src/build.rs +481 -0
  131. package/crates/tish_native/src/config.rs +48 -0
  132. package/crates/tish_native/src/lib.rs +416 -0
  133. package/crates/tish_opt/Cargo.toml +13 -0
  134. package/crates/tish_opt/src/lib.rs +1046 -0
  135. package/crates/tish_parser/Cargo.toml +11 -0
  136. package/crates/tish_parser/src/lib.rs +386 -0
  137. package/crates/tish_parser/src/parser.rs +2726 -0
  138. package/crates/tish_pg/Cargo.toml +34 -0
  139. package/crates/tish_pg/README.md +38 -0
  140. package/crates/tish_pg/src/error.rs +52 -0
  141. package/crates/tish_pg/src/lib.rs +955 -0
  142. package/crates/tish_resolve/Cargo.toml +13 -0
  143. package/crates/tish_resolve/src/lib.rs +3601 -0
  144. package/crates/tish_resolve/src/pos.rs +141 -0
  145. package/crates/tish_runtime/Cargo.toml +100 -0
  146. package/crates/tish_runtime/src/http.rs +1347 -0
  147. package/crates/tish_runtime/src/http_fetch.rs +492 -0
  148. package/crates/tish_runtime/src/http_hyper.rs +441 -0
  149. package/crates/tish_runtime/src/http_prefork.rs +189 -0
  150. package/crates/tish_runtime/src/lib.rs +1447 -0
  151. package/crates/tish_runtime/src/native_promise.rs +15 -0
  152. package/crates/tish_runtime/src/promise.rs +558 -0
  153. package/crates/tish_runtime/src/promise_io.rs +38 -0
  154. package/crates/tish_runtime/src/timers.rs +172 -0
  155. package/crates/tish_runtime/src/tty.rs +226 -0
  156. package/crates/tish_runtime/src/ws.rs +778 -0
  157. package/crates/tish_runtime/tests/fetch_readable_stream.rs +102 -0
  158. package/crates/tish_ui/Cargo.toml +17 -0
  159. package/crates/tish_ui/src/jsx.rs +692 -0
  160. package/crates/tish_ui/src/lib.rs +20 -0
  161. package/crates/tish_ui/src/runtime/hooks.rs +573 -0
  162. package/crates/tish_ui/src/runtime/mod.rs +183 -0
  163. package/crates/tish_vm/Cargo.toml +60 -0
  164. package/crates/tish_vm/src/jit.rs +1050 -0
  165. package/crates/tish_vm/src/lib.rs +41 -0
  166. package/crates/tish_vm/src/vm.rs +3536 -0
  167. package/crates/tish_vm/tests/concurrent_shared_state.rs +140 -0
  168. package/crates/tish_vm/tests/fixtures/or_string_cmd.tish +2 -0
  169. package/crates/tish_vm/tests/lexical_scope_declare.rs +34 -0
  170. package/crates/tish_vm/tests/peephole_jump_chain_logical_or.rs +150 -0
  171. package/crates/tish_wasm/Cargo.toml +15 -0
  172. package/crates/tish_wasm/src/lib.rs +428 -0
  173. package/crates/tish_wasm_runtime/Cargo.toml +37 -0
  174. package/crates/tish_wasm_runtime/src/gpu.rs +429 -0
  175. package/crates/tish_wasm_runtime/src/lib.rs +42 -0
  176. package/crates/tishlang_cargo_bindgen/Cargo.toml +26 -0
  177. package/crates/tishlang_cargo_bindgen/src/classify.rs +261 -0
  178. package/crates/tishlang_cargo_bindgen/src/discover.rs +125 -0
  179. package/crates/tishlang_cargo_bindgen/src/infer.rs +382 -0
  180. package/crates/tishlang_cargo_bindgen/src/lib.rs +349 -0
  181. package/crates/tishlang_cargo_bindgen/src/main.rs +167 -0
  182. package/crates/tishlang_cargo_bindgen/src/metadata.rs +117 -0
  183. package/justfile +276 -0
  184. package/package.json +2 -2
  185. package/platform/darwin-arm64/tish-fmt +0 -0
  186. package/platform/darwin-x64/tish-fmt +0 -0
  187. package/platform/linux-arm64/tish-fmt +0 -0
  188. package/platform/linux-x64/tish-fmt +0 -0
  189. package/platform/win32-x64/tish-fmt.exe +0 -0
@@ -0,0 +1,1459 @@
1
+ //! Tish Language Server — diagnostics, symbols, completion, format, go-to-definition, workspace symbols.
2
+
3
+ use std::collections::HashMap;
4
+ use std::path::PathBuf;
5
+ use std::sync::{Arc, RwLock};
6
+
7
+ use regex::Regex;
8
+ use tower_lsp::jsonrpc::Result;
9
+ use tower_lsp::lsp_types::{
10
+ CompletionItem, CompletionItemKind, CompletionParams, CompletionResponse,
11
+ CompletionTriggerKind, Diagnostic, DiagnosticSeverity, DiagnosticTag,
12
+ DidChangeTextDocumentParams, DidCloseTextDocumentParams, DidOpenTextDocumentParams,
13
+ DocumentFormattingParams, DocumentSymbol, DocumentSymbolParams, DocumentSymbolResponse,
14
+ GotoDefinitionParams, GotoDefinitionResponse, Hover, HoverContents, HoverParams,
15
+ HoverProviderCapability, InitializeParams, InitializeResult, Location, MarkupContent,
16
+ MarkupKind, MessageType, NumberOrString, OneOf, Position, Range, ReferenceParams,
17
+ RenameOptions, RenameParams, ServerCapabilities, ServerInfo, SymbolInformation, SymbolKind,
18
+ SymbolTag, TextDocumentPositionParams, TextDocumentSyncCapability, TextDocumentSyncKind, Url,
19
+ WorkDoneProgressOptions, WorkspaceEdit, WorkspaceSymbolParams,
20
+ };
21
+ use tower_lsp::lsp_types::{PrepareRenameResponse, TextEdit};
22
+ use tower_lsp::{Client, LanguageServer, LspService, Server};
23
+ use walkdir::WalkDir;
24
+
25
+ mod builtin_goto;
26
+ mod import_goto;
27
+
28
+ #[derive(Debug)]
29
+ struct Backend {
30
+ client: Client,
31
+ docs: Arc<RwLock<HashMap<Url, String>>>,
32
+ roots: Arc<RwLock<Vec<PathBuf>>>,
33
+ /// `(project_root, cargo:spec)` → resolved dependency source root (for `cargo metadata` / registry).
34
+ cargo_src_cache: Arc<RwLock<HashMap<(PathBuf, String), PathBuf>>>,
35
+ /// Root of the `tishlang/tish` checkout (parent of `crates/`), for built-in / JSX goto-definition.
36
+ tishlang_source_root: Arc<RwLock<Option<PathBuf>>>,
37
+ }
38
+
39
+ #[tokio::main]
40
+ async fn main() {
41
+ let stdin = tokio::io::stdin();
42
+ let stdout = tokio::io::stdout();
43
+
44
+ let (service, socket) = LspService::new(|client| Backend {
45
+ client,
46
+ docs: Arc::new(RwLock::new(HashMap::new())),
47
+ roots: Arc::new(RwLock::new(Vec::new())),
48
+ cargo_src_cache: Arc::new(RwLock::new(HashMap::new())),
49
+ tishlang_source_root: Arc::new(RwLock::new(None)),
50
+ });
51
+ Server::new(stdin, stdout, socket).serve(service).await;
52
+ }
53
+
54
+ fn parse_error_pos(err: &str) -> (u32, u32) {
55
+ static RE: std::sync::OnceLock<Regex> = std::sync::OnceLock::new();
56
+ let re = RE.get_or_init(|| Regex::new(r"start: \((\d+), (\d+)\)").unwrap());
57
+ if let Some(c) = re.captures(err) {
58
+ let line: u32 = c.get(1).and_then(|m| m.as_str().parse().ok()).unwrap_or(1);
59
+ let col: u32 = c.get(2).and_then(|m| m.as_str().parse().ok()).unwrap_or(1);
60
+ return (line.saturating_sub(1), col.saturating_sub(1));
61
+ }
62
+ (0, 0)
63
+ }
64
+
65
+ fn pos(line: u32, col: u32) -> Position {
66
+ Position {
67
+ line,
68
+ character: col,
69
+ }
70
+ }
71
+
72
+ fn diag_range(line: u32, col: u32, text: &str) -> Range {
73
+ let line_str = text.lines().nth(line as usize).unwrap_or("");
74
+ let end_char = line_str.len().max(col as usize + 1) as u32;
75
+ Range {
76
+ start: pos(line, col),
77
+ end: pos(line, end_char.min(col + 80)),
78
+ }
79
+ }
80
+
81
+ /// `lsp-types` still requires the `deprecated` field on these structs, but marks it
82
+ /// `#[deprecated(note = "Use tags instead")]`. Use `tags` with [`SymbolTag::Deprecated`] when a
83
+ /// symbol is actually deprecated; this helper keeps a single `#[allow(deprecated)]` boundary.
84
+ #[allow(deprecated)]
85
+ fn symbol_information(
86
+ name: String,
87
+ kind: SymbolKind,
88
+ tags: Option<Vec<SymbolTag>>,
89
+ location: Location,
90
+ container_name: Option<String>,
91
+ ) -> SymbolInformation {
92
+ SymbolInformation {
93
+ name,
94
+ kind,
95
+ tags,
96
+ deprecated: None,
97
+ location,
98
+ container_name,
99
+ }
100
+ }
101
+
102
+ #[allow(deprecated)]
103
+ fn document_symbol(
104
+ name: String,
105
+ detail: Option<String>,
106
+ kind: SymbolKind,
107
+ tags: Option<Vec<SymbolTag>>,
108
+ range: Range,
109
+ selection_range: Range,
110
+ children: Option<Vec<DocumentSymbol>>,
111
+ ) -> DocumentSymbol {
112
+ DocumentSymbol {
113
+ name,
114
+ detail,
115
+ kind,
116
+ tags,
117
+ deprecated: None,
118
+ range,
119
+ selection_range,
120
+ children,
121
+ }
122
+ }
123
+
124
+ async fn publish_parse_and_lint(client: &Client, uri: Url, text: &str) {
125
+ let mut diags = Vec::new();
126
+ match tishlang_parser::parse(text) {
127
+ Ok(program) => {
128
+ for d in tishlang_lint::lint_program(&program) {
129
+ let sev = match d.severity {
130
+ tishlang_lint::Severity::Error => DiagnosticSeverity::ERROR,
131
+ tishlang_lint::Severity::Warning => DiagnosticSeverity::WARNING,
132
+ };
133
+ diags.push(Diagnostic {
134
+ range: diag_range(d.line.saturating_sub(1), d.col.saturating_sub(1), text),
135
+ severity: Some(sev),
136
+ code: Some(NumberOrString::String(d.code.to_string())),
137
+ message: d.message,
138
+ ..Default::default()
139
+ });
140
+ }
141
+ for u in tishlang_resolve::collect_unresolved_identifiers(&program) {
142
+ diags.push(Diagnostic {
143
+ range: span_to_range(&u.span, text),
144
+ severity: Some(DiagnosticSeverity::ERROR),
145
+ code: Some(NumberOrString::String("tish-unresolved-name".into())),
146
+ message: format!("no binding in scope for `{}`", u.name),
147
+ ..Default::default()
148
+ });
149
+ }
150
+ for ub in tishlang_resolve::collect_unused_bindings(&program, text) {
151
+ let (message, code) = match ub.kind {
152
+ tishlang_resolve::UnusedBindingKind::Import => (
153
+ format!("`{}` is imported but never used", ub.name),
154
+ "tish-unused-import",
155
+ ),
156
+ tishlang_resolve::UnusedBindingKind::Parameter => (
157
+ format!("`{}` is declared but never read", ub.name),
158
+ "tish-unused-parameter",
159
+ ),
160
+ tishlang_resolve::UnusedBindingKind::Variable => (
161
+ format!("`{}` is declared but its value is never read", ub.name),
162
+ "tish-unused-variable",
163
+ ),
164
+ };
165
+ diags.push(Diagnostic {
166
+ range: span_to_range(&ub.span, text),
167
+ severity: Some(DiagnosticSeverity::HINT),
168
+ code: Some(NumberOrString::String(code.into())),
169
+ message,
170
+ tags: Some(vec![DiagnosticTag::UNNECESSARY]),
171
+ source: Some("tish".into()),
172
+ ..Default::default()
173
+ });
174
+ }
175
+ // Gradual type checker (Phase 2): surface provable annotation violations as warnings.
176
+ for d in tishlang_compile::check_program(&program) {
177
+ diags.push(Diagnostic {
178
+ range: span_to_range(&d.span, text),
179
+ severity: Some(DiagnosticSeverity::WARNING),
180
+ code: Some(NumberOrString::String("tish-type".into())),
181
+ message: d.message,
182
+ source: Some("tish".into()),
183
+ ..Default::default()
184
+ });
185
+ }
186
+ }
187
+ Err(e) => {
188
+ let (l, c) = parse_error_pos(&e);
189
+ diags.push(Diagnostic {
190
+ range: diag_range(l, c, text),
191
+ severity: Some(DiagnosticSeverity::ERROR),
192
+ message: e,
193
+ ..Default::default()
194
+ });
195
+ }
196
+ }
197
+ // MUST be awaited — `publish_diagnostics` is async; a bare `let _ = …` drops the future
198
+ // unsent, which silently disables ALL LSP diagnostics (parse errors, lints, unused bindings).
199
+ client.publish_diagnostics(uri, diags, None).await;
200
+ }
201
+
202
+ #[tower_lsp::async_trait]
203
+ impl LanguageServer for Backend {
204
+ async fn initialize(&self, params: InitializeParams) -> Result<InitializeResult> {
205
+ let mut roots = self.roots.write().unwrap();
206
+ roots.clear();
207
+ if let Some(folders) = params.workspace_folders {
208
+ for f in folders {
209
+ if let Ok(p) = f.uri.to_file_path() {
210
+ roots.push(p);
211
+ }
212
+ }
213
+ } else if let Some(uri) = params.root_uri {
214
+ if let Ok(p) = uri.to_file_path() {
215
+ roots.push(p);
216
+ }
217
+ }
218
+
219
+ let mut src_root: Option<PathBuf> = None;
220
+ if let Some(opts) = &params.initialization_options {
221
+ if let Some(s) = opts
222
+ .get("tishlangSourceRoot")
223
+ .and_then(|v| v.as_str())
224
+ .map(str::trim)
225
+ {
226
+ if !s.is_empty() {
227
+ src_root = Some(PathBuf::from(s));
228
+ }
229
+ }
230
+ }
231
+ if src_root.is_none() {
232
+ if let Ok(s) = std::env::var("TISHLANG_SOURCE_ROOT") {
233
+ let t = s.trim();
234
+ if !t.is_empty() {
235
+ src_root = Some(PathBuf::from(t));
236
+ }
237
+ }
238
+ }
239
+ let mut g = self.tishlang_source_root.write().unwrap();
240
+ *g = src_root.filter(|p| p.is_dir());
241
+
242
+ Ok(InitializeResult {
243
+ capabilities: ServerCapabilities {
244
+ text_document_sync: Some(TextDocumentSyncCapability::Kind(
245
+ TextDocumentSyncKind::FULL,
246
+ )),
247
+ completion_provider: Some(tower_lsp::lsp_types::CompletionOptions {
248
+ trigger_characters: Some(vec![".".to_string()]),
249
+ ..Default::default()
250
+ }),
251
+ hover_provider: Some(HoverProviderCapability::Simple(true)),
252
+ definition_provider: Some(OneOf::Left(true)),
253
+ references_provider: Some(OneOf::Left(true)),
254
+ rename_provider: Some(OneOf::Right(RenameOptions {
255
+ prepare_provider: Some(true),
256
+ work_done_progress_options: WorkDoneProgressOptions::default(),
257
+ })),
258
+ document_formatting_provider: Some(OneOf::Left(true)),
259
+ document_symbol_provider: Some(OneOf::Left(true)),
260
+ workspace_symbol_provider: Some(OneOf::Left(true)),
261
+ ..Default::default()
262
+ },
263
+ server_info: Some(ServerInfo {
264
+ name: "tish-lsp".into(),
265
+ version: Some(env!("CARGO_PKG_VERSION").into()),
266
+ }),
267
+ })
268
+ }
269
+
270
+ async fn initialized(&self, _: tower_lsp::lsp_types::InitializedParams) {
271
+ self.client
272
+ .log_message(MessageType::INFO, "tish-lsp ready")
273
+ .await;
274
+ }
275
+
276
+ async fn shutdown(&self) -> Result<()> {
277
+ Ok(())
278
+ }
279
+
280
+ async fn did_open(&self, p: DidOpenTextDocumentParams) {
281
+ let uri = p.text_document.uri;
282
+ let text = p.text_document.text;
283
+ self.docs.write().unwrap().insert(uri.clone(), text.clone());
284
+ publish_parse_and_lint(&self.client, uri, &text).await;
285
+ }
286
+
287
+ async fn did_change(&self, p: DidChangeTextDocumentParams) {
288
+ let uri = p.text_document.uri;
289
+ if let Some(chg) = p.content_changes.into_iter().last() {
290
+ self.docs
291
+ .write()
292
+ .unwrap()
293
+ .insert(uri.clone(), chg.text.clone());
294
+ publish_parse_and_lint(&self.client, uri, &chg.text).await;
295
+ }
296
+ }
297
+
298
+ async fn did_close(&self, p: DidCloseTextDocumentParams) {
299
+ self.docs.write().unwrap().remove(&p.text_document.uri);
300
+ self.client
301
+ .publish_diagnostics(p.text_document.uri, vec![], None)
302
+ .await;
303
+ }
304
+
305
+ async fn completion(&self, params: CompletionParams) -> Result<Option<CompletionResponse>> {
306
+ let uri = params.text_document_position.text_document.uri.clone();
307
+ let pos = params.text_document_position.position;
308
+ let text = {
309
+ let g = self.docs.read().unwrap();
310
+ g.get(&uri).cloned()
311
+ };
312
+ let Some(text) = text else {
313
+ return Ok(None);
314
+ };
315
+
316
+ let keywords = [
317
+ "fn", "async", "let", "const", "if", "else", "while", "for", "return", "break",
318
+ "continue", "switch", "case", "default", "try", "catch", "finally", "throw", "import",
319
+ "export", "from", "typeof", "void", "await", "of", "in", "true", "false", "null",
320
+ "function", "do",
321
+ ];
322
+ let mut items: Vec<CompletionItem> = keywords
323
+ .iter()
324
+ .map(|k| CompletionItem {
325
+ label: (*k).to_string(),
326
+ kind: Some(CompletionItemKind::KEYWORD),
327
+ ..Default::default()
328
+ })
329
+ .collect();
330
+
331
+ if let Ok(program) = tishlang_parser::parse(&text) {
332
+ for name in tishlang_resolve::completion_value_names_at_cursor(
333
+ &program,
334
+ &text,
335
+ pos.line,
336
+ pos.character,
337
+ ) {
338
+ items.push(CompletionItem {
339
+ label: name.to_string(),
340
+ kind: Some(value_completion_kind(&program, name.as_ref())),
341
+ ..Default::default()
342
+ });
343
+ }
344
+ }
345
+
346
+ if let Some(ctx) = params.context {
347
+ if matches!(ctx.trigger_kind, CompletionTriggerKind::TRIGGER_CHARACTER)
348
+ && ctx.trigger_character.as_deref() == Some(".")
349
+ {
350
+ // After dot: could add member completion later
351
+ }
352
+ }
353
+
354
+ Ok(Some(CompletionResponse::Array(items)))
355
+ }
356
+
357
+ async fn document_symbol(
358
+ &self,
359
+ params: DocumentSymbolParams,
360
+ ) -> Result<Option<DocumentSymbolResponse>> {
361
+ let uri = params.text_document.uri;
362
+ let text = {
363
+ let g = self.docs.read().unwrap();
364
+ g.get(&uri).cloned()
365
+ };
366
+ let Some(text) = text else {
367
+ return Ok(None);
368
+ };
369
+ let Ok(program) = tishlang_parser::parse(&text) else {
370
+ return Ok(None);
371
+ };
372
+
373
+ let mut syms: Vec<DocumentSymbol> = Vec::new();
374
+ for s in &program.statements {
375
+ doc_symbol_stmt(s, &text, &mut syms);
376
+ }
377
+ Ok(Some(DocumentSymbolResponse::Nested(syms)))
378
+ }
379
+
380
+ async fn goto_definition(
381
+ &self,
382
+ params: GotoDefinitionParams,
383
+ ) -> Result<Option<GotoDefinitionResponse>> {
384
+ let TextDocumentPositionParams {
385
+ text_document,
386
+ position,
387
+ } = params.text_document_position_params;
388
+ let uri = text_document.uri;
389
+ let text = {
390
+ let g = self.docs.read().unwrap();
391
+ g.get(&uri).cloned()
392
+ };
393
+ let Some(text) = text else {
394
+ return Ok(None);
395
+ };
396
+ let Ok(program) = tishlang_parser::parse(&text) else {
397
+ return Ok(None);
398
+ };
399
+
400
+ if let Some(def) =
401
+ tishlang_resolve::definition_span(&program, &text, position.line, position.character)
402
+ {
403
+ let range = span_to_range(&def, &text);
404
+ return Ok(Some(GotoDefinitionResponse::Scalar(Location {
405
+ uri: uri.clone(),
406
+ range,
407
+ })));
408
+ }
409
+
410
+ let word = word_at_position(&text, position);
411
+ if word.is_empty() {
412
+ return Ok(None);
413
+ }
414
+
415
+ // Type reference (`: SomeType`, `extends SomeType`, `as SomeType`) → jump to its
416
+ // `type`/`interface` declaration. Value bindings are resolved above, so this only
417
+ // fires for genuine type names.
418
+ if let Some(sp) = type_decl_span(&program, word.as_str()) {
419
+ return Ok(Some(GotoDefinitionResponse::Scalar(Location {
420
+ uri: uri.clone(),
421
+ range: span_to_range(&sp, &text),
422
+ })));
423
+ }
424
+
425
+ if let Ok(ref file_path) = uri.to_file_path() {
426
+ let roots = self.roots.read().unwrap().clone();
427
+ if let Some(loc) = import_goto::definition_for_import(
428
+ &program,
429
+ file_path,
430
+ word.as_str(),
431
+ &roots,
432
+ self.cargo_src_cache.as_ref(),
433
+ ) {
434
+ return Ok(Some(GotoDefinitionResponse::Scalar(loc)));
435
+ }
436
+ if let Some(loc) = import_goto::definition_for_native_receiver_member(
437
+ &program,
438
+ file_path,
439
+ &text,
440
+ &roots,
441
+ self.cargo_src_cache.as_ref(),
442
+ position.line,
443
+ position.character,
444
+ word.as_str(),
445
+ ) {
446
+ return Ok(Some(GotoDefinitionResponse::Scalar(loc)));
447
+ }
448
+ }
449
+
450
+ if let Some(root) = self.tishlang_source_root.read().unwrap().clone() {
451
+ if let Some(bdef) = builtin_goto::definition_for_builtin(
452
+ &text,
453
+ position.line,
454
+ position.character,
455
+ word.as_str(),
456
+ ) {
457
+ if let Some(loc) = builtin_goto::to_file_location(&root, &bdef) {
458
+ return Ok(Some(GotoDefinitionResponse::Scalar(loc)));
459
+ }
460
+ }
461
+ }
462
+
463
+ Ok(None)
464
+ }
465
+
466
+ async fn hover(&self, params: HoverParams) -> Result<Option<Hover>> {
467
+ let pos = params.text_document_position_params.position;
468
+ let uri = params.text_document_position_params.text_document.uri;
469
+ let text = {
470
+ let g = self.docs.read().unwrap();
471
+ g.get(&uri).cloned()
472
+ };
473
+ let Some(text) = text else {
474
+ return Ok(None);
475
+ };
476
+ let Ok(program) = tishlang_parser::parse(&text) else {
477
+ return Ok(None);
478
+ };
479
+ let Some(use_site) =
480
+ tishlang_resolve::name_at_cursor(&program, &text, pos.line, pos.character)
481
+ else {
482
+ // Not a value name at the cursor — it may be a type reference (`: SomeType`,
483
+ // `extends SomeType`). Type annotations carry no spans, so match by word.
484
+ let word = word_at_position(&text, pos);
485
+ if let Some(ty) = type_alias_body(&program, &word) {
486
+ let value = format!("**`{}`**{}", word, code_hint(&format!("type {} = {}", word, ty)));
487
+ return Ok(Some(Hover {
488
+ range: None,
489
+ contents: HoverContents::Markup(MarkupContent {
490
+ kind: MarkupKind::Markdown,
491
+ value,
492
+ }),
493
+ }));
494
+ }
495
+ return Ok(None);
496
+ };
497
+ let def = tishlang_resolve::definition_span(&program, &text, pos.line, pos.character);
498
+ let mut md = format!("**`{}`**", use_site.name);
499
+ // Type-aware hover: show the declared (or simply-inferred) type / fn signature.
500
+ if let Some(ref dspan) = def {
501
+ if let Some(hint) = type_hint_at_def(&program, dspan) {
502
+ md.push_str(&hint);
503
+ }
504
+ }
505
+ match def {
506
+ Some(def) if def.start == use_site.span.start && def.end == use_site.span.end => {
507
+ md.push_str("\n\n_(binding site)_");
508
+ }
509
+ Some(def) => {
510
+ md.push_str(&format!(
511
+ "\n\nDefined at line {} col {}",
512
+ def.start.0, def.start.1
513
+ ));
514
+ }
515
+ None => {
516
+ if tishlang_resolve::is_runtime_global_ident(use_site.name.as_ref()) {
517
+ md.push_str(
518
+ "\n\n_Interpreter root global (no lexical declaration in this file)._",
519
+ );
520
+ let word = word_at_position(&text, pos);
521
+ if !word.is_empty() {
522
+ if let Some(root) = self.tishlang_source_root.read().unwrap().clone() {
523
+ if let Some(bdef) = builtin_goto::definition_for_builtin(
524
+ &text,
525
+ pos.line,
526
+ pos.character,
527
+ word.as_str(),
528
+ ) {
529
+ if let Some(loc) = builtin_goto::to_file_location(&root, &bdef) {
530
+ // VS Code treats `#L<1-based-line>` on file URLs like "go to line".
531
+ let line_1 = bdef.line.saturating_add(1);
532
+ let href = loc.uri.as_str();
533
+ md.push_str(&format!(
534
+ "\n\n[Open in Tish sources]({href}#L{line_1}) (`{}`)",
535
+ bdef.rel_path
536
+ ));
537
+ }
538
+ }
539
+ }
540
+ }
541
+ } else {
542
+ let word = word_at_position(&text, pos);
543
+ if word.is_empty() {
544
+ md.push_str("\n\n_No binding in scope for this name._");
545
+ } else if let Ok(fp) = uri.to_file_path() {
546
+ let roots = self.roots.read().unwrap().clone();
547
+ if let Some(nmd) = import_goto::native_member_definition(
548
+ &program,
549
+ &fp,
550
+ &text,
551
+ &roots,
552
+ self.cargo_src_cache.as_ref(),
553
+ pos.line,
554
+ pos.character,
555
+ word.as_str(),
556
+ ) {
557
+ md.push_str(
558
+ "\n\n_Native host module member (e.g. `tish:macos`); implementation in Rust._",
559
+ );
560
+ if let Some(ref d) = nmd.doc {
561
+ md.push_str("\n\n");
562
+ md.push_str(d);
563
+ }
564
+ let loc = nmd.location;
565
+ let line_1 = loc.range.start.line.saturating_add(1);
566
+ let href = loc.uri.as_str();
567
+ md.push_str(&format!(
568
+ "\n\n[Open Rust implementation]({href}#L{line_1})"
569
+ ));
570
+ } else {
571
+ md.push_str("\n\n_No binding in scope for this name._");
572
+ }
573
+ } else {
574
+ md.push_str("\n\n_No binding in scope for this name._");
575
+ }
576
+ }
577
+ }
578
+ }
579
+ Ok(Some(Hover {
580
+ range: Some(span_to_range(&use_site.span, &text)),
581
+ contents: HoverContents::Markup(MarkupContent {
582
+ kind: MarkupKind::Markdown,
583
+ value: md,
584
+ }),
585
+ }))
586
+ }
587
+
588
+ async fn references(&self, params: ReferenceParams) -> Result<Option<Vec<Location>>> {
589
+ let pos = params.text_document_position.position;
590
+ let uri = params.text_document_position.text_document.uri;
591
+ let text = {
592
+ let g = self.docs.read().unwrap();
593
+ g.get(&uri).cloned()
594
+ };
595
+ let Some(text) = text else {
596
+ return Ok(None);
597
+ };
598
+ let Ok(program) = tishlang_parser::parse(&text) else {
599
+ return Ok(None);
600
+ };
601
+ let Some(def) = tishlang_resolve::definition_span(&program, &text, pos.line, pos.character)
602
+ else {
603
+ return Ok(None);
604
+ };
605
+ let Some(nu) = tishlang_resolve::name_at_cursor(&program, &text, pos.line, pos.character)
606
+ else {
607
+ return Ok(None);
608
+ };
609
+ let spans =
610
+ tishlang_resolve::reference_spans_for_def(&program, &text, nu.name.as_ref(), def);
611
+ let locs: Vec<Location> = spans
612
+ .into_iter()
613
+ .map(|sp| Location {
614
+ uri: uri.clone(),
615
+ range: span_to_range(&sp, &text),
616
+ })
617
+ .collect();
618
+ Ok(Some(locs))
619
+ }
620
+
621
+ async fn prepare_rename(
622
+ &self,
623
+ params: TextDocumentPositionParams,
624
+ ) -> Result<Option<PrepareRenameResponse>> {
625
+ let pos = params.position;
626
+ let uri = params.text_document.uri;
627
+ let text = {
628
+ let g = self.docs.read().unwrap();
629
+ g.get(&uri).cloned()
630
+ };
631
+ let Some(text) = text else {
632
+ return Ok(None);
633
+ };
634
+ let Ok(program) = tishlang_parser::parse(&text) else {
635
+ return Ok(None);
636
+ };
637
+ let Some(nu) = tishlang_resolve::name_at_cursor(&program, &text, pos.line, pos.character)
638
+ else {
639
+ return Ok(None);
640
+ };
641
+ let range = span_to_range(&nu.span, &text);
642
+ Ok(Some(PrepareRenameResponse::RangeWithPlaceholder {
643
+ range,
644
+ placeholder: nu.name.to_string(),
645
+ }))
646
+ }
647
+
648
+ async fn rename(&self, params: RenameParams) -> Result<Option<WorkspaceEdit>> {
649
+ let pos = params.text_document_position.position;
650
+ let uri = params.text_document_position.text_document.uri;
651
+ let new_name = params.new_name;
652
+ let text = {
653
+ let g = self.docs.read().unwrap();
654
+ g.get(&uri).cloned()
655
+ };
656
+ let Some(text) = text else {
657
+ return Ok(None);
658
+ };
659
+ let Ok(program) = tishlang_parser::parse(&text) else {
660
+ return Ok(None);
661
+ };
662
+ let Some(def) = tishlang_resolve::definition_span(&program, &text, pos.line, pos.character)
663
+ else {
664
+ return Ok(None);
665
+ };
666
+ let Some(nu) = tishlang_resolve::name_at_cursor(&program, &text, pos.line, pos.character)
667
+ else {
668
+ return Ok(None);
669
+ };
670
+ let spans =
671
+ tishlang_resolve::reference_spans_for_def(&program, &text, nu.name.as_ref(), def);
672
+ let mut edits: Vec<TextEdit> = spans
673
+ .into_iter()
674
+ .map(|sp| TextEdit {
675
+ range: span_to_range(&sp, &text),
676
+ new_text: new_name.clone(),
677
+ })
678
+ .collect();
679
+ // Apply from end of document so earlier ranges stay valid when lengths change.
680
+ edits.sort_by(|a, b| {
681
+ (b.range.start.line, b.range.start.character)
682
+ .cmp(&(a.range.start.line, a.range.start.character))
683
+ });
684
+ let mut m = HashMap::new();
685
+ m.insert(uri, edits);
686
+ Ok(Some(WorkspaceEdit {
687
+ changes: Some(m),
688
+ ..Default::default()
689
+ }))
690
+ }
691
+
692
+ async fn formatting(
693
+ &self,
694
+ params: DocumentFormattingParams,
695
+ ) -> Result<Option<Vec<tower_lsp::lsp_types::TextEdit>>> {
696
+ let uri = params.text_document.uri;
697
+ let text = {
698
+ let g = self.docs.read().unwrap();
699
+ g.get(&uri).cloned()
700
+ };
701
+ let Some(text) = text else {
702
+ return Ok(None);
703
+ };
704
+ match tishlang_fmt::format_source(&text) {
705
+ Ok(formatted) => {
706
+ let lines = text.lines().count() as u32;
707
+ let last_line = text.lines().last().map(|l| l.len() as u32).unwrap_or(0);
708
+ Ok(Some(vec![tower_lsp::lsp_types::TextEdit {
709
+ range: Range {
710
+ start: pos(0, 0),
711
+ end: pos(lines.saturating_sub(1), last_line),
712
+ },
713
+ new_text: formatted,
714
+ }]))
715
+ }
716
+ Err(e) => {
717
+ self.client
718
+ .show_message(MessageType::ERROR, format!("tish-fmt (formatter): {}", e))
719
+ .await;
720
+ Ok(None)
721
+ }
722
+ }
723
+ }
724
+
725
+ async fn symbol(
726
+ &self,
727
+ params: WorkspaceSymbolParams,
728
+ ) -> Result<Option<Vec<SymbolInformation>>> {
729
+ let query = params.query.to_lowercase();
730
+ if query.is_empty() {
731
+ return Ok(Some(vec![]));
732
+ }
733
+ let roots = self.roots.read().unwrap().clone();
734
+ let mut out = Vec::new();
735
+
736
+ for root in roots {
737
+ for e in WalkDir::new(&root)
738
+ .into_iter()
739
+ .filter_map(|e| e.ok())
740
+ .filter(|e| e.path().extension().map(|x| x == "tish").unwrap_or(false))
741
+ {
742
+ let path = e.path();
743
+ let Ok(src) = std::fs::read_to_string(path) else {
744
+ continue;
745
+ };
746
+ let Ok(program) = tishlang_parser::parse(&src) else {
747
+ continue;
748
+ };
749
+ let Ok(uri) = Url::from_file_path(path) else {
750
+ continue;
751
+ };
752
+ for s in &program.statements {
753
+ collect_workspace_syms(s, &src, &uri, &query, &mut out);
754
+ }
755
+ }
756
+ }
757
+ Ok(Some(out))
758
+ }
759
+ }
760
+
761
+ fn collect_workspace_syms(
762
+ s: &tishlang_ast::Statement,
763
+ text: &str,
764
+ uri: &Url,
765
+ query: &str,
766
+ out: &mut Vec<SymbolInformation>,
767
+ ) {
768
+ match s {
769
+ tishlang_ast::Statement::FunDecl {
770
+ name, name_span, ..
771
+ } => {
772
+ if name.to_lowercase().contains(query) {
773
+ out.push(symbol_information(
774
+ name.to_string(),
775
+ SymbolKind::FUNCTION,
776
+ None,
777
+ Location {
778
+ uri: uri.clone(),
779
+ range: span_to_range(name_span, text),
780
+ },
781
+ None,
782
+ ));
783
+ }
784
+ }
785
+ tishlang_ast::Statement::VarDecl {
786
+ name, name_span, ..
787
+ } => {
788
+ if name.to_lowercase().contains(query) {
789
+ out.push(symbol_information(
790
+ name.to_string(),
791
+ SymbolKind::VARIABLE,
792
+ None,
793
+ Location {
794
+ uri: uri.clone(),
795
+ range: span_to_range(name_span, text),
796
+ },
797
+ None,
798
+ ));
799
+ }
800
+ }
801
+ tishlang_ast::Statement::Block { statements, .. } => {
802
+ for x in statements {
803
+ collect_workspace_syms(x, text, uri, query, out);
804
+ }
805
+ }
806
+ _ => {}
807
+ }
808
+ }
809
+
810
+ pub(crate) fn find_export(
811
+ program: &tishlang_ast::Program,
812
+ name: &str,
813
+ uri: &Url,
814
+ text: &str,
815
+ ) -> Option<Location> {
816
+ for s in &program.statements {
817
+ match s {
818
+ tishlang_ast::Statement::FunDecl {
819
+ name: n, name_span, ..
820
+ } if n.as_ref() == name => {
821
+ return Some(Location {
822
+ uri: uri.clone(),
823
+ range: span_to_range(name_span, text),
824
+ });
825
+ }
826
+ tishlang_ast::Statement::VarDecl {
827
+ name: n, name_span, ..
828
+ } if n.as_ref() == name => {
829
+ return Some(Location {
830
+ uri: uri.clone(),
831
+ range: span_to_range(name_span, text),
832
+ });
833
+ }
834
+ tishlang_ast::Statement::Export { declaration, .. } => if let tishlang_ast::ExportDeclaration::Named(inner) = declaration.as_ref() {
835
+ if let Some(loc) = find_decl_in_stmt(inner, name, uri, text) {
836
+ return Some(loc);
837
+ }
838
+ },
839
+ _ => {}
840
+ }
841
+ }
842
+ None
843
+ }
844
+
845
+ fn find_decl_in_stmt(
846
+ s: &tishlang_ast::Statement,
847
+ word: &str,
848
+ uri: &Url,
849
+ text: &str,
850
+ ) -> Option<Location> {
851
+ match s {
852
+ tishlang_ast::Statement::FunDecl {
853
+ name, name_span, ..
854
+ } if name.as_ref() == word => Some(Location {
855
+ uri: uri.clone(),
856
+ range: span_to_range(name_span, text),
857
+ }),
858
+ tishlang_ast::Statement::VarDecl {
859
+ name, name_span, ..
860
+ } if name.as_ref() == word => Some(Location {
861
+ uri: uri.clone(),
862
+ range: span_to_range(name_span, text),
863
+ }),
864
+ tishlang_ast::Statement::Block { statements, .. } => {
865
+ for x in statements {
866
+ if let Some(l) = find_decl_in_stmt(x, word, uri, text) {
867
+ return Some(l);
868
+ }
869
+ }
870
+ None
871
+ }
872
+ _ => None,
873
+ }
874
+ }
875
+
876
+ fn span_to_range(span: &tishlang_ast::Span, text: &str) -> Range {
877
+ if let Some(((sl, sc), (el, ec))) = tishlang_resolve::span_to_lsp_range_exclusive(text, span) {
878
+ Range {
879
+ start: pos(sl, sc),
880
+ end: pos(el, ec),
881
+ }
882
+ } else {
883
+ Range {
884
+ start: pos(
885
+ span.start.0.saturating_sub(1) as u32,
886
+ span.start.1.saturating_sub(1) as u32,
887
+ ),
888
+ end: pos(
889
+ span.end.0.saturating_sub(1) as u32,
890
+ span.end.1.saturating_sub(1) as u32,
891
+ ),
892
+ }
893
+ }
894
+ }
895
+
896
+ fn word_at_position(text: &str, position: Position) -> String {
897
+ let line = text.lines().nth(position.line as usize).unwrap_or("");
898
+ let chars: Vec<(usize, char)> = line.char_indices().collect();
899
+ let col = (position.character as usize).min(chars.len());
900
+ // Pick the identifier the cursor is on. If the cursor sits just past a word's end
901
+ // (on whitespace/punct or EOL), fall back to the identifier immediately to its left.
902
+ let mut start = col;
903
+ if start >= chars.len() || !is_ident_char(chars[start].1) {
904
+ if start == 0 || !is_ident_char(chars[start - 1].1) {
905
+ return String::new();
906
+ }
907
+ start -= 1;
908
+ }
909
+ // Scan left to the word start, then right to the word end (the original missed the prefix
910
+ // when the cursor landed in the middle of a word).
911
+ while start > 0 && is_ident_char(chars[start - 1].1) {
912
+ start -= 1;
913
+ }
914
+ let mut end = start;
915
+ while end < chars.len() && is_ident_char(chars[end].1) {
916
+ end += 1;
917
+ }
918
+ let s = chars[start].0;
919
+ let e = chars.get(end).map(|(p, _)| *p).unwrap_or(line.len());
920
+ line[s..e].to_string()
921
+ }
922
+
923
+ fn is_ident_char(c: char) -> bool {
924
+ c.is_alphanumeric() || c == '_'
925
+ }
926
+
927
+ // ── Type-aware hover ─────────────────────────────────────────────────────────
928
+
929
+ /// Render a `TypeAnnotation` to a readable, TypeScript-ish string for hover.
930
+ fn render_type(t: &tishlang_ast::TypeAnnotation) -> String {
931
+ use tishlang_ast::{TypeAnnotation as T, TypeLiteral as L};
932
+ match t {
933
+ T::Simple(s) => s.to_string(),
934
+ T::Array(inner) => {
935
+ // Parenthesize composite element types so `(A | B)[]` reads unambiguously.
936
+ if matches!(
937
+ inner.as_ref(),
938
+ T::Union(_) | T::Intersection(_) | T::Function { .. }
939
+ ) {
940
+ format!("({})[]", render_type(inner))
941
+ } else {
942
+ format!("{}[]", render_type(inner))
943
+ }
944
+ }
945
+ T::Object(fields) => format!(
946
+ "{{ {} }}",
947
+ fields
948
+ .iter()
949
+ .map(|(k, v)| format!("{}: {}", k, render_type(v)))
950
+ .collect::<Vec<_>>()
951
+ .join(", ")
952
+ ),
953
+ T::Function { params, returns } => format!(
954
+ "({}) => {}",
955
+ params.iter().map(render_type).collect::<Vec<_>>().join(", "),
956
+ render_type(returns)
957
+ ),
958
+ T::Union(ts) => ts.iter().map(render_type).collect::<Vec<_>>().join(" | "),
959
+ T::Tuple(ts) => format!(
960
+ "[{}]",
961
+ ts.iter().map(render_type).collect::<Vec<_>>().join(", ")
962
+ ),
963
+ T::Intersection(ts) => ts.iter().map(render_type).collect::<Vec<_>>().join(" & "),
964
+ T::Literal(L::Str(s)) => format!("\"{}\"", s),
965
+ T::Literal(L::Num(n)) => {
966
+ if n.fract() == 0.0 && n.is_finite() {
967
+ format!("{}", *n as i64)
968
+ } else {
969
+ n.to_string()
970
+ }
971
+ }
972
+ T::Literal(L::Bool(b)) => b.to_string(),
973
+ }
974
+ }
975
+
976
+ /// Best-effort type of a simple initializer (literals only). Anything non-trivial returns `None`,
977
+ /// so hover omits the type rather than guessing wrong.
978
+ fn shallow_expr_type(e: &tishlang_ast::Expr) -> Option<tishlang_ast::TypeAnnotation> {
979
+ use tishlang_ast::{Expr, Literal, TypeAnnotation as T};
980
+ if let Expr::Literal { value, .. } = e {
981
+ let name = match value {
982
+ Literal::Number(_) => "number",
983
+ Literal::String(_) => "string",
984
+ Literal::Bool(_) => "boolean",
985
+ Literal::Null => "null",
986
+ };
987
+ Some(T::Simple(Arc::from(name)))
988
+ } else {
989
+ None
990
+ }
991
+ }
992
+
993
+ /// Render a function parameter as `name: T` (or just `name` when unannotated).
994
+ fn render_param(p: &tishlang_ast::FunParam) -> String {
995
+ use tishlang_ast::FunParam;
996
+ match p {
997
+ FunParam::Simple(tp) => match &tp.type_ann {
998
+ Some(t) => format!("{}: {}", tp.name, render_type(t)),
999
+ None => tp.name.to_string(),
1000
+ },
1001
+ FunParam::Destructure { type_ann, .. } => match type_ann {
1002
+ Some(t) => format!("{{…}}: {}", render_type(t)),
1003
+ None => "{…}".to_string(),
1004
+ },
1005
+ }
1006
+ }
1007
+
1008
+ /// `fn name(params): R` signature line for a function declaration.
1009
+ fn fn_signature(
1010
+ name: &str,
1011
+ params: &[tishlang_ast::FunParam],
1012
+ rest: &Option<tishlang_ast::TypedParam>,
1013
+ ret: &Option<tishlang_ast::TypeAnnotation>,
1014
+ ) -> String {
1015
+ let mut ps: Vec<String> = params.iter().map(render_param).collect();
1016
+ if let Some(r) = rest {
1017
+ let t = r
1018
+ .type_ann
1019
+ .as_ref()
1020
+ .map(|t| format!(": {}", render_type(t)))
1021
+ .unwrap_or_default();
1022
+ ps.push(format!("...{}{}", r.name, t));
1023
+ }
1024
+ let ret_s = ret
1025
+ .as_ref()
1026
+ .map(render_type)
1027
+ .unwrap_or_else(|| "void".to_string());
1028
+ format!("fn {}({}): {}", name, ps.join(", "), ret_s)
1029
+ }
1030
+
1031
+ /// Definition spans are name spans; match on the start position.
1032
+ fn same_start(a: &tishlang_ast::Span, b: &tishlang_ast::Span) -> bool {
1033
+ a.start == b.start
1034
+ }
1035
+
1036
+ /// Wrap a one-line type hint in a tish code fence for hover.
1037
+ fn code_hint(line: &str) -> String {
1038
+ format!("\n\n```tish\n{}\n```", line)
1039
+ }
1040
+
1041
+ /// Find the declaration whose name is at `def` and produce a hover type line (markdown), if any.
1042
+ fn type_hint_at_def(program: &tishlang_ast::Program, def: &tishlang_ast::Span) -> Option<String> {
1043
+ program.statements.iter().find_map(|s| hint_in_stmt(s, def))
1044
+ }
1045
+
1046
+ /// `name_span` of a `type`/`interface` declaration named `name` (both parse to `TypeAlias`).
1047
+ /// Used so cmd+click on a `: SomeType` reference jumps to its declaration. Type annotations
1048
+ /// carry no spans, so this is a name match — sound because value bindings resolve first.
1049
+ fn type_decl_span(program: &tishlang_ast::Program, name: &str) -> Option<tishlang_ast::Span> {
1050
+ program.statements.iter().find_map(|s| match s {
1051
+ tishlang_ast::Statement::TypeAlias {
1052
+ name: n, name_span, ..
1053
+ } if n.as_ref() == name => Some(*name_span),
1054
+ _ => None,
1055
+ })
1056
+ }
1057
+
1058
+ /// Rendered body of a `type`/`interface` declaration named `name`, for hover.
1059
+ fn type_alias_body(program: &tishlang_ast::Program, name: &str) -> Option<String> {
1060
+ program.statements.iter().find_map(|s| match s {
1061
+ tishlang_ast::Statement::TypeAlias { name: n, ty, .. } if n.as_ref() == name => {
1062
+ Some(render_type(ty))
1063
+ }
1064
+ _ => None,
1065
+ })
1066
+ }
1067
+
1068
+ fn hint_in_stmt(s: &tishlang_ast::Statement, def: &tishlang_ast::Span) -> Option<String> {
1069
+ use tishlang_ast::{FunParam, Statement as St};
1070
+ match s {
1071
+ St::VarDecl {
1072
+ name,
1073
+ name_span,
1074
+ mutable,
1075
+ type_ann,
1076
+ init,
1077
+ ..
1078
+ } => {
1079
+ if same_start(name_span, def) {
1080
+ let ty = type_ann
1081
+ .clone()
1082
+ .or_else(|| init.as_ref().and_then(shallow_expr_type))?;
1083
+ let kw = if *mutable { "let" } else { "const" };
1084
+ return Some(code_hint(&format!(
1085
+ "{} {}: {}",
1086
+ kw,
1087
+ name,
1088
+ render_type(&ty)
1089
+ )));
1090
+ }
1091
+ None
1092
+ }
1093
+ St::FunDecl {
1094
+ name,
1095
+ name_span,
1096
+ params,
1097
+ rest_param,
1098
+ return_type,
1099
+ body,
1100
+ ..
1101
+ } => {
1102
+ if same_start(name_span, def) {
1103
+ return Some(code_hint(&fn_signature(
1104
+ name,
1105
+ params,
1106
+ rest_param,
1107
+ return_type,
1108
+ )));
1109
+ }
1110
+ for p in params {
1111
+ if let FunParam::Simple(tp) = p {
1112
+ if same_start(&tp.name_span, def) {
1113
+ let ty = tp
1114
+ .type_ann
1115
+ .as_ref()
1116
+ .map(render_type)
1117
+ .unwrap_or_else(|| "any".to_string());
1118
+ return Some(code_hint(&format!("(parameter) {}: {}", tp.name, ty)));
1119
+ }
1120
+ }
1121
+ }
1122
+ if let Some(r) = rest_param {
1123
+ if same_start(&r.name_span, def) {
1124
+ let ty = r
1125
+ .type_ann
1126
+ .as_ref()
1127
+ .map(render_type)
1128
+ .unwrap_or_else(|| "any[]".to_string());
1129
+ return Some(code_hint(&format!("(parameter) ...{}: {}", r.name, ty)));
1130
+ }
1131
+ }
1132
+ hint_in_stmt(body, def)
1133
+ }
1134
+ St::Block { statements, .. } | St::Multi { statements, .. } => {
1135
+ statements.iter().find_map(|s| hint_in_stmt(s, def))
1136
+ }
1137
+ St::If {
1138
+ then_branch,
1139
+ else_branch,
1140
+ ..
1141
+ } => hint_in_stmt(then_branch, def)
1142
+ .or_else(|| else_branch.as_ref().and_then(|e| hint_in_stmt(e, def))),
1143
+ St::For { init, body, .. } => init
1144
+ .as_ref()
1145
+ .and_then(|i| hint_in_stmt(i, def))
1146
+ .or_else(|| hint_in_stmt(body, def)),
1147
+ St::While { body, .. } | St::DoWhile { body, .. } | St::ForOf { body, .. } => {
1148
+ hint_in_stmt(body, def)
1149
+ }
1150
+ St::Try { body, .. } => hint_in_stmt(body, def),
1151
+ _ => None,
1152
+ }
1153
+ }
1154
+
1155
+ fn value_completion_kind(program: &tishlang_ast::Program, name: &str) -> CompletionItemKind {
1156
+ for s in &program.statements {
1157
+ if let Some(k) = value_completion_kind_stmt(s, name) {
1158
+ return k;
1159
+ }
1160
+ }
1161
+ CompletionItemKind::VARIABLE
1162
+ }
1163
+
1164
+ fn value_completion_kind_stmt(
1165
+ s: &tishlang_ast::Statement,
1166
+ name: &str,
1167
+ ) -> Option<CompletionItemKind> {
1168
+ match s {
1169
+ tishlang_ast::Statement::FunDecl { name: n, .. } if n.as_ref() == name => {
1170
+ Some(CompletionItemKind::FUNCTION)
1171
+ }
1172
+ tishlang_ast::Statement::VarDecl { name: n, .. } if n.as_ref() == name => {
1173
+ Some(CompletionItemKind::VARIABLE)
1174
+ }
1175
+ tishlang_ast::Statement::Import { specifiers, .. } => {
1176
+ for sp in specifiers {
1177
+ let local = match sp {
1178
+ tishlang_ast::ImportSpecifier::Named { name: n, alias, .. } => {
1179
+ alias.as_ref().map(|a| a.as_ref()).unwrap_or(n.as_ref())
1180
+ }
1181
+ tishlang_ast::ImportSpecifier::Default { name: n, .. } => n.as_ref(),
1182
+ tishlang_ast::ImportSpecifier::Namespace { name: n, .. } => n.as_ref(),
1183
+ };
1184
+ if local == name {
1185
+ return Some(CompletionItemKind::VARIABLE);
1186
+ }
1187
+ }
1188
+ None
1189
+ }
1190
+ tishlang_ast::Statement::Block { statements, .. } => statements
1191
+ .iter()
1192
+ .find_map(|x| value_completion_kind_stmt(x, name)),
1193
+ tishlang_ast::Statement::If {
1194
+ then_branch,
1195
+ else_branch,
1196
+ ..
1197
+ } => value_completion_kind_stmt(then_branch, name).or_else(|| {
1198
+ else_branch
1199
+ .as_ref()
1200
+ .and_then(|b| value_completion_kind_stmt(b, name))
1201
+ }),
1202
+ tishlang_ast::Statement::While { body, .. }
1203
+ | tishlang_ast::Statement::ForOf { body, .. }
1204
+ | tishlang_ast::Statement::DoWhile { body, .. } => value_completion_kind_stmt(body, name),
1205
+ tishlang_ast::Statement::For { init, body, .. } => init
1206
+ .as_ref()
1207
+ .and_then(|i| value_completion_kind_stmt(i, name))
1208
+ .or_else(|| value_completion_kind_stmt(body, name)),
1209
+ tishlang_ast::Statement::Try {
1210
+ body,
1211
+ catch_body,
1212
+ finally_body,
1213
+ ..
1214
+ } => value_completion_kind_stmt(body, name)
1215
+ .or_else(|| {
1216
+ catch_body
1217
+ .as_ref()
1218
+ .and_then(|b| value_completion_kind_stmt(b, name))
1219
+ })
1220
+ .or_else(|| {
1221
+ finally_body
1222
+ .as_ref()
1223
+ .and_then(|b| value_completion_kind_stmt(b, name))
1224
+ }),
1225
+ tishlang_ast::Statement::Switch {
1226
+ cases,
1227
+ default_body,
1228
+ ..
1229
+ } => {
1230
+ for (_e, stmts) in cases {
1231
+ if let Some(k) = stmts
1232
+ .iter()
1233
+ .find_map(|st| value_completion_kind_stmt(st, name))
1234
+ {
1235
+ return Some(k);
1236
+ }
1237
+ }
1238
+ default_body.as_ref().and_then(|stmts| {
1239
+ stmts
1240
+ .iter()
1241
+ .find_map(|st| value_completion_kind_stmt(st, name))
1242
+ })
1243
+ }
1244
+ tishlang_ast::Statement::Export { declaration, .. } => match declaration.as_ref() {
1245
+ tishlang_ast::ExportDeclaration::Named(inner) => {
1246
+ value_completion_kind_stmt(inner, name)
1247
+ }
1248
+ tishlang_ast::ExportDeclaration::Default(_) => None,
1249
+ },
1250
+ _ => None,
1251
+ }
1252
+ }
1253
+
1254
+ fn doc_symbol_stmt(
1255
+ s: &tishlang_ast::Statement,
1256
+ text: &str,
1257
+ out: &mut Vec<DocumentSymbol>,
1258
+ ) {
1259
+ match s {
1260
+ tishlang_ast::Statement::FunDecl {
1261
+ name,
1262
+ name_span,
1263
+ span,
1264
+ body,
1265
+ ..
1266
+ } => {
1267
+ let mut children = Vec::new();
1268
+ collect_child_syms(body, text, &mut children);
1269
+ out.push(document_symbol(
1270
+ name.to_string(),
1271
+ None,
1272
+ SymbolKind::FUNCTION,
1273
+ None,
1274
+ span_to_range(span, text),
1275
+ span_to_range(name_span, text),
1276
+ if children.is_empty() {
1277
+ None
1278
+ } else {
1279
+ Some(children)
1280
+ },
1281
+ ));
1282
+ }
1283
+ tishlang_ast::Statement::VarDecl {
1284
+ name,
1285
+ name_span,
1286
+ span,
1287
+ ..
1288
+ } => {
1289
+ out.push(document_symbol(
1290
+ name.to_string(),
1291
+ None,
1292
+ SymbolKind::VARIABLE,
1293
+ None,
1294
+ span_to_range(span, text),
1295
+ span_to_range(name_span, text),
1296
+ None,
1297
+ ));
1298
+ }
1299
+ tishlang_ast::Statement::Block { statements, .. } => {
1300
+ for x in statements {
1301
+ doc_symbol_stmt(x, text, out);
1302
+ }
1303
+ }
1304
+ _ => {}
1305
+ }
1306
+ }
1307
+
1308
+ fn collect_child_syms(
1309
+ s: &tishlang_ast::Statement,
1310
+ text: &str,
1311
+ out: &mut Vec<DocumentSymbol>,
1312
+ ) {
1313
+ match s {
1314
+ tishlang_ast::Statement::Block { statements, .. } => {
1315
+ for x in statements {
1316
+ doc_symbol_stmt(x, text, out);
1317
+ }
1318
+ }
1319
+ _ => doc_symbol_stmt(s, text, out),
1320
+ }
1321
+ }
1322
+
1323
+ #[cfg(test)]
1324
+ mod hover_tests {
1325
+ use super::*;
1326
+ use tishlang_ast::{FunParam, Span, Statement};
1327
+
1328
+ fn parse(src: &str) -> tishlang_ast::Program {
1329
+ tishlang_parser::parse(src).expect("parse")
1330
+ }
1331
+
1332
+ /// name_span of the first VarDecl/FunDecl named `name`, searched recursively.
1333
+ fn decl_span(s: &Statement, name: &str) -> Option<Span> {
1334
+ match s {
1335
+ Statement::VarDecl { name: n, name_span, .. } if n.as_ref() == name => Some(*name_span),
1336
+ Statement::FunDecl { name: n, name_span, body, .. } => {
1337
+ if n.as_ref() == name {
1338
+ Some(*name_span)
1339
+ } else {
1340
+ decl_span(body, name)
1341
+ }
1342
+ }
1343
+ Statement::Block { statements, .. } | Statement::Multi { statements, .. } => {
1344
+ statements.iter().find_map(|x| decl_span(x, name))
1345
+ }
1346
+ Statement::If { then_branch, else_branch, .. } => decl_span(then_branch, name)
1347
+ .or_else(|| else_branch.as_ref().and_then(|e| decl_span(e, name))),
1348
+ Statement::For { body, .. }
1349
+ | Statement::While { body, .. }
1350
+ | Statement::DoWhile { body, .. }
1351
+ | Statement::ForOf { body, .. } => decl_span(body, name),
1352
+ _ => None,
1353
+ }
1354
+ }
1355
+
1356
+ fn span_of(p: &tishlang_ast::Program, name: &str) -> Span {
1357
+ p.statements
1358
+ .iter()
1359
+ .find_map(|s| decl_span(s, name))
1360
+ .unwrap_or_else(|| panic!("decl `{name}` not found"))
1361
+ }
1362
+
1363
+ fn param_span(p: &tishlang_ast::Program, fname: &str, pname: &str) -> Span {
1364
+ for s in &p.statements {
1365
+ if let Statement::FunDecl { name, params, .. } = s {
1366
+ if name.as_ref() == fname {
1367
+ for fp in params {
1368
+ if let FunParam::Simple(tp) = fp {
1369
+ if tp.name.as_ref() == pname {
1370
+ return tp.name_span;
1371
+ }
1372
+ }
1373
+ }
1374
+ }
1375
+ }
1376
+ }
1377
+ panic!("param `{fname}.{pname}` not found")
1378
+ }
1379
+
1380
+ fn hint(p: &tishlang_ast::Program, span: &Span) -> String {
1381
+ type_hint_at_def(p, span).expect("expected a type hint")
1382
+ }
1383
+
1384
+ #[test]
1385
+ fn annotated_var() {
1386
+ let p = parse("let count: number = 0\n");
1387
+ assert!(hint(&p, &span_of(&p, "count")).contains("let count: number"));
1388
+ }
1389
+
1390
+ #[test]
1391
+ fn inferred_var_and_const() {
1392
+ let p = parse("let x = 42\nconst label = \"hi\"\nlet ok = true\n");
1393
+ assert!(hint(&p, &span_of(&p, "x")).contains("let x: number"));
1394
+ assert!(hint(&p, &span_of(&p, "label")).contains("const label: string"));
1395
+ assert!(hint(&p, &span_of(&p, "ok")).contains("let ok: boolean"));
1396
+ }
1397
+
1398
+ #[test]
1399
+ fn function_signature() {
1400
+ let p = parse("fn add(a: number, b: number): number { return a + b }\n");
1401
+ assert!(hint(&p, &span_of(&p, "add")).contains("fn add(a: number, b: number): number"));
1402
+ }
1403
+
1404
+ #[test]
1405
+ fn parameter_hover() {
1406
+ let p = parse("fn f(p: string) { return p }\n");
1407
+ assert!(hint(&p, &param_span(&p, "f", "p")).contains("(parameter) p: string"));
1408
+ }
1409
+
1410
+ #[test]
1411
+ fn nested_decl_resolves() {
1412
+ let p = parse("fn g() {\n let inner: boolean = true\n return inner\n}\n");
1413
+ assert!(hint(&p, &span_of(&p, "inner")).contains("let inner: boolean"));
1414
+ }
1415
+
1416
+ #[test]
1417
+ fn composite_types_render() {
1418
+ use tishlang_ast::{TypeAnnotation as T, TypeLiteral as L};
1419
+ let arr = T::Array(Box::new(T::Simple("number".into())));
1420
+ assert_eq!(render_type(&arr), "number[]");
1421
+ let tup = T::Tuple(vec![T::Simple("number".into()), T::Simple("string".into())]);
1422
+ assert_eq!(render_type(&tup), "[number, string]");
1423
+ let uni = T::Union(vec![T::Simple("number".into()), T::Simple("null".into())]);
1424
+ assert_eq!(render_type(&uni), "number | null");
1425
+ assert_eq!(render_type(&T::Literal(L::Str("on".into()))), "\"on\"");
1426
+ let arr_of_union = T::Array(Box::new(uni));
1427
+ assert_eq!(render_type(&arr_of_union), "(number | null)[]");
1428
+ }
1429
+ }
1430
+
1431
+ #[cfg(test)]
1432
+ mod type_ref_tests {
1433
+ use super::*;
1434
+
1435
+ const SRC: &str =
1436
+ "interface Point { x: number, y: number }\ntype Status = \"on\" | \"off\"\nlet p: Point = { x: 1, y: 2 }\n";
1437
+
1438
+ #[test]
1439
+ fn type_decl_lookup_and_body() {
1440
+ let p = tishlang_parser::parse(SRC).expect("parse");
1441
+ assert!(type_decl_span(&p, "Point").is_some());
1442
+ assert!(type_decl_span(&p, "Status").is_some());
1443
+ assert_eq!(type_alias_body(&p, "Point").as_deref(), Some("{ x: number, y: number }"));
1444
+ assert_eq!(type_alias_body(&p, "Status").as_deref(), Some("\"on\" | \"off\""));
1445
+ assert!(type_decl_span(&p, "Nope").is_none());
1446
+ }
1447
+
1448
+ #[test]
1449
+ fn word_at_position_finds_whole_word() {
1450
+ // Cursor in the MIDDLE of `Point` (line 2, the `o`) must yield the whole word.
1451
+ assert_eq!(word_at_position(SRC, Position { line: 2, character: 8 }), "Point");
1452
+ // At the word start.
1453
+ assert_eq!(word_at_position(SRC, Position { line: 2, character: 7 }), "Point");
1454
+ // Just past the end (on the space) falls back to the word on the left.
1455
+ assert_eq!(word_at_position(SRC, Position { line: 2, character: 12 }), "Point");
1456
+ // On punctuation between words → empty.
1457
+ assert_eq!(word_at_position("a = b\n", Position { line: 0, character: 2 }), "");
1458
+ }
1459
+ }