@tishlang/tish 2.10.1 → 2.16.13
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.
- package/Cargo.toml +3 -0
- package/bin/tish +0 -0
- package/crates/tish/Cargo.toml +1 -1
- package/crates/tish/src/cli_help.rs +16 -0
- package/crates/tish/src/main.rs +24 -4
- package/crates/tish/tests/integration_test.rs +149 -0
- package/crates/tish_ast/src/ast.rs +10 -0
- package/crates/tish_builtins/src/array.rs +529 -56
- package/crates/tish_builtins/src/collections.rs +114 -40
- package/crates/tish_builtins/src/string.rs +95 -8
- package/crates/tish_bytecode/src/chunk.rs +7 -0
- package/crates/tish_bytecode/src/compiler.rs +560 -64
- package/crates/tish_bytecode/src/lib.rs +1 -1
- package/crates/tish_bytecode/src/opcode.rs +154 -2
- package/crates/tish_bytecode/src/serialize.rs +2 -0
- package/crates/tish_bytecode/tests/append_local_string_builder.rs +63 -0
- package/crates/tish_bytecode/tests/math_unary_intrinsic.rs +47 -0
- package/crates/tish_compile/src/codegen.rs +17736 -5269
- package/crates/tish_compile/src/infer.rs +1707 -190
- package/crates/tish_compile/src/lib.rs +29 -4
- package/crates/tish_compile/src/resolve.rs +66 -5
- package/crates/tish_compile/src/types.rs +224 -28
- package/crates/tish_compile/tests/dump_codegen.rs +21 -0
- package/crates/tish_compile/tests/perf_codegen_169_173.rs +8 -17
- package/crates/tish_compile/tests/perf_codegen_173_part3.rs +61 -0
- package/crates/tish_compile/tests/perf_codegen_174.rs +160 -0
- package/crates/tish_compile/tests/perf_codegen_175.rs +190 -0
- package/crates/tish_compile/tests/perf_codegen_176.rs +60 -0
- package/crates/tish_compile/tests/perf_codegen_177.rs +167 -0
- package/crates/tish_compile/tests/perf_codegen_178.rs +114 -0
- package/crates/tish_compile/tests/perf_codegen_178_rec.rs +124 -0
- package/crates/tish_compile/tests/perf_codegen_181.rs +36 -0
- package/crates/tish_compile/tests/perf_codegen_320.rs +211 -0
- package/crates/tish_compile/tests/perf_codegen_module_const_forof.rs +40 -0
- package/crates/tish_compile_js/src/codegen.rs +91 -4
- package/crates/tish_compile_js/src/lib.rs +5 -2
- package/crates/tish_compile_js/src/tests_jsx.rs +175 -7
- package/crates/tish_compiler_wasm/src/resolve_virtual.rs +100 -2
- package/crates/tish_core/Cargo.toml +4 -0
- package/crates/tish_core/src/json.rs +104 -5
- package/crates/tish_core/src/lib.rs +174 -0
- package/crates/tish_core/src/shape.rs +4 -2
- package/crates/tish_core/src/value.rs +565 -35
- package/crates/tish_core/src/vmref.rs +14 -0
- package/crates/tish_eval/src/eval.rs +675 -76
- package/crates/tish_eval/src/natives.rs +19 -0
- package/crates/tish_eval/src/value.rs +76 -21
- package/crates/tish_ffi/src/lib.rs +11 -1
- package/crates/tish_ffi/tests/double_free.rs +35 -0
- package/crates/tish_fmt/src/lib.rs +75 -1
- package/crates/tish_lexer/src/lib.rs +76 -0
- package/crates/tish_lexer/src/token.rs +4 -0
- package/crates/tish_lint/src/lib.rs +126 -0
- package/crates/tish_lsp/README.md +2 -1
- package/crates/tish_lsp/src/main.rs +378 -28
- package/crates/tish_native/src/build.rs +41 -0
- package/crates/tish_parser/Cargo.toml +4 -0
- package/crates/tish_parser/src/lib.rs +27 -0
- package/crates/tish_parser/src/parser.rs +302 -20
- package/crates/tish_resolve/src/lib.rs +9 -0
- package/crates/tish_runtime/Cargo.toml +5 -0
- package/crates/tish_runtime/src/http.rs +28 -10
- package/crates/tish_runtime/src/http_fetch.rs +150 -1
- package/crates/tish_runtime/src/http_prefork.rs +72 -11
- package/crates/tish_runtime/src/lib.rs +523 -42
- package/crates/tish_runtime/src/timers.rs +49 -2
- package/crates/tish_ui/src/jsx.rs +253 -0
- package/crates/tish_vm/src/jit.rs +2514 -117
- package/crates/tish_vm/src/vm.rs +1227 -182
- package/package.json +1 -1
- package/platform/darwin-arm64/tish +0 -0
- package/platform/darwin-x64/tish +0 -0
- package/platform/linux-arm64/tish +0 -0
- package/platform/linux-x64/tish +0 -0
- package/platform/win32-x64/tish.exe +0 -0
|
@@ -2,24 +2,29 @@
|
|
|
2
2
|
|
|
3
3
|
use std::collections::{HashMap, HashSet};
|
|
4
4
|
use std::path::PathBuf;
|
|
5
|
+
use std::sync::atomic::{AtomicBool, Ordering};
|
|
5
6
|
use std::sync::{Arc, Mutex, RwLock};
|
|
6
7
|
use std::time::SystemTime;
|
|
7
8
|
|
|
8
9
|
use regex::Regex;
|
|
9
10
|
use tower_lsp::jsonrpc::Result;
|
|
11
|
+
use tower_lsp::lsp_types::notification::Progress;
|
|
10
12
|
use tower_lsp::lsp_types::{
|
|
11
13
|
CompletionItem, CompletionItemKind, CompletionParams, CompletionResponse,
|
|
12
14
|
CompletionTriggerKind, Diagnostic, DiagnosticSeverity, DiagnosticTag,
|
|
13
|
-
DidChangeTextDocumentParams,
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
15
|
+
DidChangeTextDocumentParams, DidChangeWatchedFilesParams,
|
|
16
|
+
DidChangeWatchedFilesRegistrationOptions, DidChangeWorkspaceFoldersParams,
|
|
17
|
+
DidCloseTextDocumentParams, DidOpenTextDocumentParams, DocumentFormattingParams,
|
|
18
|
+
DocumentSymbol, DocumentSymbolParams, DocumentSymbolResponse, FileSystemWatcher, GlobPattern,
|
|
19
|
+
GotoDefinitionParams, GotoDefinitionResponse, Hover, HoverContents, HoverParams,
|
|
20
|
+
HoverProviderCapability, InitializeParams, InitializeResult, Location, MarkupContent,
|
|
21
|
+
MarkupKind, MessageType, NumberOrString, OneOf, Position, ProgressParams, ProgressParamsValue,
|
|
22
|
+
Range, ReferenceParams, Registration, RenameOptions, RenameParams,
|
|
23
|
+
ServerCapabilities, ServerInfo, SymbolInformation, SymbolKind, SymbolTag,
|
|
24
|
+
TextDocumentPositionParams, TextDocumentSyncCapability, TextDocumentSyncKind, Url,
|
|
25
|
+
WorkDoneProgress, WorkDoneProgressBegin, WorkDoneProgressEnd, WorkDoneProgressOptions,
|
|
26
|
+
WorkspaceEdit, WorkspaceFoldersChangeEvent, WorkspaceFoldersServerCapabilities,
|
|
27
|
+
WorkspaceServerCapabilities, WorkspaceSymbolOptions, WorkspaceSymbolParams,
|
|
23
28
|
};
|
|
24
29
|
use tower_lsp::lsp_types::{PrepareRenameResponse, TextEdit};
|
|
25
30
|
use tower_lsp::{Client, LanguageServer, LspService, Server};
|
|
@@ -51,6 +56,33 @@ struct Backend {
|
|
|
51
56
|
/// based on divergent root snapshots. Held only across the (blocking) refresh, off the async
|
|
52
57
|
/// driver (#162).
|
|
53
58
|
symbol_refresh: Arc<Mutex<()>>,
|
|
59
|
+
/// Whether the client advertised `window.workDoneProgress` at `initialize`. Only then may the
|
|
60
|
+
/// server emit `$/progress` work-done notifications for long requests; otherwise a compliant
|
|
61
|
+
/// client would reject them (#164).
|
|
62
|
+
client_work_done_progress: Arc<RwLock<bool>>,
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/// RAII cancel flag for a long-running request. tower-lsp aborts the request handler future on
|
|
66
|
+
/// `$/cancelRequest` (or when the client otherwise drops interest), which drops everything the
|
|
67
|
+
/// handler holds. Dropping this guard flips the shared flag, so blocking work handed to
|
|
68
|
+
/// `spawn_blocking` — which the runtime cannot itself abort — observes the cancellation and bails
|
|
69
|
+
/// early instead of running the whole walk and publishing a now-stale result (#164).
|
|
70
|
+
struct CancelGuard(Arc<AtomicBool>);
|
|
71
|
+
|
|
72
|
+
impl CancelGuard {
|
|
73
|
+
fn new() -> Self {
|
|
74
|
+
CancelGuard(Arc::new(AtomicBool::new(false)))
|
|
75
|
+
}
|
|
76
|
+
/// A handle the blocking work polls; stays live after the guard drops.
|
|
77
|
+
fn flag(&self) -> Arc<AtomicBool> {
|
|
78
|
+
Arc::clone(&self.0)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
impl Drop for CancelGuard {
|
|
83
|
+
fn drop(&mut self) {
|
|
84
|
+
self.0.store(true, Ordering::Relaxed);
|
|
85
|
+
}
|
|
54
86
|
}
|
|
55
87
|
|
|
56
88
|
/// One symbol harvested from a workspace file, with its position pre-resolved so answering a query
|
|
@@ -87,6 +119,7 @@ async fn main() {
|
|
|
87
119
|
tishlang_source_root: Arc::new(RwLock::new(None)),
|
|
88
120
|
symbol_index: Arc::new(RwLock::new(HashMap::new())),
|
|
89
121
|
symbol_refresh: Arc::new(Mutex::new(())),
|
|
122
|
+
client_work_done_progress: Arc::new(RwLock::new(false)),
|
|
90
123
|
});
|
|
91
124
|
Server::new(stdin, stdout, socket).serve(service).await;
|
|
92
125
|
}
|
|
@@ -332,6 +365,18 @@ impl LanguageServer for Backend {
|
|
|
332
365
|
}
|
|
333
366
|
let mut g = self.tishlang_source_root.write().unwrap();
|
|
334
367
|
*g = src_root.filter(|p| p.is_dir());
|
|
368
|
+
drop(g);
|
|
369
|
+
|
|
370
|
+
// Remember whether the client can display work-done progress. Only then does `symbol` emit
|
|
371
|
+
// `$/progress` begin/report/end for the (potentially slow) workspace crawl — a client that
|
|
372
|
+
// did not advertise it would reject the notifications (#164).
|
|
373
|
+
let supports_progress = params
|
|
374
|
+
.capabilities
|
|
375
|
+
.window
|
|
376
|
+
.as_ref()
|
|
377
|
+
.and_then(|w| w.work_done_progress)
|
|
378
|
+
.unwrap_or(false);
|
|
379
|
+
*self.client_work_done_progress.write().unwrap() = supports_progress;
|
|
335
380
|
|
|
336
381
|
Ok(InitializeResult {
|
|
337
382
|
capabilities: ServerCapabilities {
|
|
@@ -351,7 +396,15 @@ impl LanguageServer for Backend {
|
|
|
351
396
|
})),
|
|
352
397
|
document_formatting_provider: Some(OneOf::Left(true)),
|
|
353
398
|
document_symbol_provider: Some(OneOf::Left(true)),
|
|
354
|
-
|
|
399
|
+
// Advertise work-done progress on workspace/symbol so a capable client sends a
|
|
400
|
+
// `workDoneToken`; the handler reports crawl progress against it and honors
|
|
401
|
+
// `$/cancelRequest` mid-walk (#164).
|
|
402
|
+
workspace_symbol_provider: Some(OneOf::Right(WorkspaceSymbolOptions {
|
|
403
|
+
work_done_progress_options: WorkDoneProgressOptions {
|
|
404
|
+
work_done_progress: Some(true),
|
|
405
|
+
},
|
|
406
|
+
resolve_provider: None,
|
|
407
|
+
})),
|
|
355
408
|
// Ask the client to send didChangeWorkspaceFolders so adding/removing a folder in a
|
|
356
409
|
// multi-root workspace keeps `roots` (and thus the workspace-symbol index) accurate
|
|
357
410
|
// instead of being frozen at the set captured by `initialize` (#162).
|
|
@@ -372,6 +425,35 @@ impl LanguageServer for Backend {
|
|
|
372
425
|
}
|
|
373
426
|
|
|
374
427
|
async fn initialized(&self, _: tower_lsp::lsp_types::InitializedParams) {
|
|
428
|
+
// Dynamically register for `workspace/didChangeWatchedFiles` over `.tish` and `.d.tish`
|
|
429
|
+
// files so an externally edited declaration (`.d.tish`) or source file refreshes the
|
|
430
|
+
// workspace-symbol index without a server restart (#161). The client only delivers these
|
|
431
|
+
// events for globs the server registers; a `.tish` glob does NOT cover `.d.tish` (it has a
|
|
432
|
+
// compound extension), so both are registered explicitly.
|
|
433
|
+
let watchers = |glob: &str| FileSystemWatcher {
|
|
434
|
+
glob_pattern: GlobPattern::String(glob.to_string()),
|
|
435
|
+
kind: None, // create | change | delete
|
|
436
|
+
};
|
|
437
|
+
let reg = Registration {
|
|
438
|
+
id: "tish-watch-d-tish".to_string(),
|
|
439
|
+
method: "workspace/didChangeWatchedFiles".to_string(),
|
|
440
|
+
register_options: serde_json::to_value(DidChangeWatchedFilesRegistrationOptions {
|
|
441
|
+
watchers: vec![watchers("**/*.tish"), watchers("**/*.d.tish")],
|
|
442
|
+
})
|
|
443
|
+
.ok(),
|
|
444
|
+
};
|
|
445
|
+
// Best-effort: a client that does not support dynamic watched-file registration returns an
|
|
446
|
+
// error here, which we log and move on from — the mtime-keyed index still self-heals on the
|
|
447
|
+
// next `workspace/symbol` query.
|
|
448
|
+
if let Err(e) = self.client.register_capability(vec![reg]).await {
|
|
449
|
+
self.client
|
|
450
|
+
.log_message(
|
|
451
|
+
MessageType::INFO,
|
|
452
|
+
format!("tish-lsp: watched-files registration skipped: {e}"),
|
|
453
|
+
)
|
|
454
|
+
.await;
|
|
455
|
+
}
|
|
456
|
+
|
|
375
457
|
self.client
|
|
376
458
|
.log_message(MessageType::INFO, "tish-lsp ready")
|
|
377
459
|
.await;
|
|
@@ -390,6 +472,48 @@ impl LanguageServer for Backend {
|
|
|
390
472
|
.await;
|
|
391
473
|
}
|
|
392
474
|
|
|
475
|
+
async fn did_change_watched_files(&self, params: DidChangeWatchedFilesParams) {
|
|
476
|
+
// React to on-disk `.tish` / `.d.tish` changes the editor is not buffering (created,
|
|
477
|
+
// externally edited, or deleted) so cross-file declarations refresh without a restart
|
|
478
|
+
// (#161). Drop the changed path from the mtime-keyed symbol index: a deleted file is
|
|
479
|
+
// evicted immediately, and a created/changed one is force-reparsed on the next
|
|
480
|
+
// `workspace/symbol` walk (its cache entry is gone, so the mtime shortcut can't reuse a
|
|
481
|
+
// stale parse). `.d.tish` files carry the `tish` extension, so both are handled here.
|
|
482
|
+
let mut invalidated = 0usize;
|
|
483
|
+
{
|
|
484
|
+
let mut idx = self.symbol_index.write().unwrap();
|
|
485
|
+
for change in ¶ms.changes {
|
|
486
|
+
if let Ok(path) = change.uri.to_file_path() {
|
|
487
|
+
if path.extension().map(|x| x == "tish") == Some(true)
|
|
488
|
+
&& idx.remove(&path).is_some()
|
|
489
|
+
{
|
|
490
|
+
invalidated += 1;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
// Re-lint any OPEN buffer whose declarations may now have changed. A `.d.tish` supplies
|
|
496
|
+
// ambient `declare` bindings other buffers depend on, so a change there can flip
|
|
497
|
+
// unresolved-name diagnostics; recompute open docs against the new on-disk state.
|
|
498
|
+
let open: Vec<(Url, String)> = {
|
|
499
|
+
let docs = self.docs.read().unwrap();
|
|
500
|
+
docs.iter().map(|(u, t)| (u.clone(), t.clone())).collect()
|
|
501
|
+
};
|
|
502
|
+
for (uri, text) in open {
|
|
503
|
+
publish_parse_and_lint(&self.client, uri, text).await;
|
|
504
|
+
}
|
|
505
|
+
self.client
|
|
506
|
+
.log_message(
|
|
507
|
+
MessageType::INFO,
|
|
508
|
+
format!(
|
|
509
|
+
"tish-lsp: watched files changed ({} index entr{} invalidated)",
|
|
510
|
+
invalidated,
|
|
511
|
+
if invalidated == 1 { "y" } else { "ies" }
|
|
512
|
+
),
|
|
513
|
+
)
|
|
514
|
+
.await;
|
|
515
|
+
}
|
|
516
|
+
|
|
393
517
|
async fn shutdown(&self) -> Result<()> {
|
|
394
518
|
Ok(())
|
|
395
519
|
}
|
|
@@ -422,13 +546,27 @@ impl LanguageServer for Backend {
|
|
|
422
546
|
let edit_seq = Arc::clone(&self.edit_seq);
|
|
423
547
|
tokio::spawn(async move {
|
|
424
548
|
tokio::time::sleep(std::time::Duration::from_millis(200)).await;
|
|
549
|
+
// Still the latest edit? (superseded-while-debouncing check.)
|
|
550
|
+
let is_current = |uri: &Url, seq: u64| {
|
|
551
|
+
edit_seq.read().unwrap().get(uri).copied() == Some(seq)
|
|
552
|
+
};
|
|
425
553
|
// Superseded by a newer edit while we waited — drop this stale recompute.
|
|
426
|
-
if
|
|
554
|
+
if !is_current(&uri, seq) {
|
|
427
555
|
return;
|
|
428
556
|
}
|
|
429
557
|
let text = docs.read().unwrap().get(&uri).cloned();
|
|
430
558
|
if let Some(text) = text {
|
|
431
|
-
|
|
559
|
+
// Compute off the async driver, then re-check the sequence: analysis is O(file
|
|
560
|
+
// size) and a keystroke can land while it runs, so publishing unconditionally
|
|
561
|
+
// would show diagnostics for text the user has already moved past (#164). Bail
|
|
562
|
+
// before publishing if this recompute is no longer the latest.
|
|
563
|
+
let diags = tokio::task::spawn_blocking(move || compute_diagnostics(&text))
|
|
564
|
+
.await
|
|
565
|
+
.unwrap_or_default();
|
|
566
|
+
if !is_current(&uri, seq) {
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
client.publish_diagnostics(uri, diags, None).await;
|
|
432
570
|
}
|
|
433
571
|
});
|
|
434
572
|
}
|
|
@@ -957,22 +1095,88 @@ impl LanguageServer for Backend {
|
|
|
957
1095
|
let roots_handle = Arc::clone(&self.roots);
|
|
958
1096
|
let index = self.symbol_index.clone();
|
|
959
1097
|
let refresh = Arc::clone(&self.symbol_refresh);
|
|
1098
|
+
|
|
1099
|
+
// Cancellation (#164). tower-lsp aborts THIS future on `$/cancelRequest`, dropping the guard
|
|
1100
|
+
// and flipping its flag; the flag lives on into the (un-abortable) blocking walk below, which
|
|
1101
|
+
// polls it and bails so we stop burning a blocking thread on a result nobody will read. The
|
|
1102
|
+
// clone kept in this future is what the drop-on-abort sets.
|
|
1103
|
+
let cancel = CancelGuard::new();
|
|
1104
|
+
let cancel_flag = cancel.flag();
|
|
1105
|
+
|
|
1106
|
+
// Work-done progress (#164): only when the client advertised support AND handed us a token to
|
|
1107
|
+
// report against. Bracket the blocking walk with begin/end `$/progress` notifications.
|
|
1108
|
+
let progress_token = params.work_done_progress_params.work_done_token;
|
|
1109
|
+
let show_progress = *self.client_work_done_progress.read().unwrap();
|
|
1110
|
+
let progress_token = progress_token.filter(|_| show_progress);
|
|
1111
|
+
if let Some(ref token) = progress_token {
|
|
1112
|
+
self.send_work_done(
|
|
1113
|
+
token.clone(),
|
|
1114
|
+
WorkDoneProgress::Begin(WorkDoneProgressBegin {
|
|
1115
|
+
title: "Searching workspace symbols".to_string(),
|
|
1116
|
+
cancellable: Some(true),
|
|
1117
|
+
message: Some(format!("query: {query}")),
|
|
1118
|
+
percentage: None,
|
|
1119
|
+
}),
|
|
1120
|
+
)
|
|
1121
|
+
.await;
|
|
1122
|
+
}
|
|
1123
|
+
|
|
960
1124
|
// The crawl + fs-read + parse is CPU/IO-blocking; running it inline stalls tower-lsp's shared
|
|
961
1125
|
// request driver (it multiplexes requests without a per-request spawn), so hover/completion
|
|
962
1126
|
// in flight would freeze for its duration. Hand it to a blocking thread, and answer from the
|
|
963
1127
|
// mtime-keyed index so repeated keystrokes don't re-walk and re-parse the tree (#135).
|
|
964
|
-
let
|
|
1128
|
+
let walk_flag = Arc::clone(&cancel_flag);
|
|
1129
|
+
let result = tokio::task::spawn_blocking(move || {
|
|
965
1130
|
// Serialize refreshes and read `roots` fresh under that lock (#162): folders can now
|
|
966
1131
|
// change mid-session, and the index's global retain would otherwise let a query carrying
|
|
967
1132
|
// a stale/smaller root snapshot evict another concurrent query's still-valid entries.
|
|
968
1133
|
// One walk at a time, each over the current roots, makes that impossible.
|
|
969
1134
|
let _refresh_guard = refresh.lock().unwrap_or_else(|e| e.into_inner());
|
|
970
1135
|
let roots = roots_handle.read().unwrap().clone();
|
|
971
|
-
refresh_and_query_symbols(&roots, &index, &query)
|
|
1136
|
+
refresh_and_query_symbols(&roots, &index, &query, &walk_flag)
|
|
972
1137
|
})
|
|
973
1138
|
.await
|
|
974
|
-
.
|
|
975
|
-
|
|
1139
|
+
.unwrap_or(WalkOutcome::Cancelled);
|
|
1140
|
+
|
|
1141
|
+
if let Some(ref token) = progress_token {
|
|
1142
|
+
self.send_work_done(
|
|
1143
|
+
token.clone(),
|
|
1144
|
+
WorkDoneProgress::End(WorkDoneProgressEnd {
|
|
1145
|
+
message: Some(match &result {
|
|
1146
|
+
WalkOutcome::Completed(s) => format!("{} symbol(s)", s.len()),
|
|
1147
|
+
WalkOutcome::Cancelled => "cancelled".to_string(),
|
|
1148
|
+
}),
|
|
1149
|
+
}),
|
|
1150
|
+
)
|
|
1151
|
+
.await;
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
// Explicitly consume the guard here so it lives across the whole request (not dropped early
|
|
1155
|
+
// by NLL); if we were aborted, this line never runs and the guard's Drop already fired.
|
|
1156
|
+
drop(cancel);
|
|
1157
|
+
|
|
1158
|
+
match result {
|
|
1159
|
+
WalkOutcome::Completed(syms) => Ok(Some(syms)),
|
|
1160
|
+
// The walk saw the cancel flag and stopped early: report cancellation rather than an
|
|
1161
|
+
// empty/partial list the client might cache as authoritative (#164).
|
|
1162
|
+
WalkOutcome::Cancelled => Err(tower_lsp::jsonrpc::Error::request_cancelled()),
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
impl Backend {
|
|
1168
|
+
/// Send a single `$/progress` work-done notification against a client-provided token.
|
|
1169
|
+
async fn send_work_done(
|
|
1170
|
+
&self,
|
|
1171
|
+
token: tower_lsp::lsp_types::ProgressToken,
|
|
1172
|
+
value: WorkDoneProgress,
|
|
1173
|
+
) {
|
|
1174
|
+
self.client
|
|
1175
|
+
.send_notification::<Progress>(ProgressParams {
|
|
1176
|
+
token,
|
|
1177
|
+
value: ProgressParamsValue::WorkDone(value),
|
|
1178
|
+
})
|
|
1179
|
+
.await;
|
|
976
1180
|
}
|
|
977
1181
|
}
|
|
978
1182
|
|
|
@@ -988,14 +1192,29 @@ fn ws_prune_dir(e: &walkdir::DirEntry) -> bool {
|
|
|
988
1192
|
name == "node_modules" || name == "target" || name.starts_with('.')
|
|
989
1193
|
}
|
|
990
1194
|
|
|
1195
|
+
/// Outcome of a workspace-symbol walk: the completed result set, or an early cancellation when the
|
|
1196
|
+
/// request was aborted mid-crawl (#164). Kept distinct so `symbol` can answer a cancelled walk with
|
|
1197
|
+
/// `RequestCancelled` rather than a partial list the client might treat as authoritative.
|
|
1198
|
+
#[derive(Debug)]
|
|
1199
|
+
enum WalkOutcome {
|
|
1200
|
+
Completed(Vec<SymbolInformation>),
|
|
1201
|
+
Cancelled,
|
|
1202
|
+
}
|
|
1203
|
+
|
|
991
1204
|
/// Refresh the mtime-keyed symbol index for `roots` (re-parsing only changed/new files, evicting
|
|
992
1205
|
/// deleted ones) and return the symbols whose lowercased name contains `query`. Runs on a blocking
|
|
993
1206
|
/// thread. Factored out as a free function so it is unit-testable without a live `Backend` (#135).
|
|
1207
|
+
///
|
|
1208
|
+
/// `cancel` is polled at each directory entry and before the (comparatively cheap) final scan; when
|
|
1209
|
+
/// it flips (the request was cancelled — see [`CancelGuard`]) the walk stops early and returns
|
|
1210
|
+
/// [`WalkOutcome::Cancelled`] instead of running the whole tree and building a stale result (#164).
|
|
1211
|
+
/// Any files already re-parsed before the abort stay cached, so the work is not wasted.
|
|
994
1212
|
fn refresh_and_query_symbols(
|
|
995
1213
|
roots: &[PathBuf],
|
|
996
1214
|
index: &RwLock<HashMap<PathBuf, CachedFile>>,
|
|
997
1215
|
query: &str,
|
|
998
|
-
|
|
1216
|
+
cancel: &AtomicBool,
|
|
1217
|
+
) -> WalkOutcome {
|
|
999
1218
|
let mut seen: HashSet<PathBuf> = HashSet::new();
|
|
1000
1219
|
for root in roots {
|
|
1001
1220
|
for e in WalkDir::new(root)
|
|
@@ -1003,6 +1222,11 @@ fn refresh_and_query_symbols(
|
|
|
1003
1222
|
.filter_entry(|e| !ws_prune_dir(e))
|
|
1004
1223
|
.filter_map(|e| e.ok())
|
|
1005
1224
|
{
|
|
1225
|
+
// Bail as soon as the client cancels: on a large tree the remaining walk+parse is pure
|
|
1226
|
+
// waste once nobody is waiting for the answer.
|
|
1227
|
+
if cancel.load(Ordering::Relaxed) {
|
|
1228
|
+
return WalkOutcome::Cancelled;
|
|
1229
|
+
}
|
|
1006
1230
|
if !e.file_type().is_file()
|
|
1007
1231
|
|| e.path().extension().map(|x| x == "tish") != Some(true)
|
|
1008
1232
|
{
|
|
@@ -1035,7 +1259,14 @@ fn refresh_and_query_symbols(
|
|
|
1035
1259
|
index.write().unwrap().insert(path, CachedFile { mtime, uri, symbols });
|
|
1036
1260
|
}
|
|
1037
1261
|
}
|
|
1038
|
-
//
|
|
1262
|
+
// A cancel that lands right as the walk finishes must not run the eviction below: `seen` is only
|
|
1263
|
+
// complete because every root was fully visited, but the caller no longer wants a result, so
|
|
1264
|
+
// stop before mutating the shared index on its behalf.
|
|
1265
|
+
if cancel.load(Ordering::Relaxed) {
|
|
1266
|
+
return WalkOutcome::Cancelled;
|
|
1267
|
+
}
|
|
1268
|
+
// Evict files that vanished from the tree so deleted symbols don't linger in results. Only safe
|
|
1269
|
+
// because the loop above ran to completion (no early cancel), so `seen` is the full file set.
|
|
1039
1270
|
index.write().unwrap().retain(|p, _| seen.contains(p));
|
|
1040
1271
|
|
|
1041
1272
|
// Answer the query from the now-fresh index.
|
|
@@ -1057,7 +1288,7 @@ fn refresh_and_query_symbols(
|
|
|
1057
1288
|
}
|
|
1058
1289
|
}
|
|
1059
1290
|
}
|
|
1060
|
-
out
|
|
1291
|
+
WalkOutcome::Completed(out)
|
|
1061
1292
|
}
|
|
1062
1293
|
|
|
1063
1294
|
/// Harvest every top-level (and exported / block-nested) named declaration from a statement into the
|
|
@@ -1617,6 +1848,9 @@ fn value_completion_kind_stmt(
|
|
|
1617
1848
|
value_completion_kind_stmt(inner, name)
|
|
1618
1849
|
}
|
|
1619
1850
|
tishlang_ast::ExportDeclaration::Default(_) => None,
|
|
1851
|
+
// A re-exported name (`export { x } from "./m"` / `export * from`) is bound from another
|
|
1852
|
+
// module, not declared here, so there is no local completion-kind to report.
|
|
1853
|
+
tishlang_ast::ExportDeclaration::ReExport { .. } => None,
|
|
1620
1854
|
},
|
|
1621
1855
|
_ => None,
|
|
1622
1856
|
}
|
|
@@ -2083,6 +2317,7 @@ mod jsonrpc_integration_tests {
|
|
|
2083
2317
|
tishlang_source_root: Arc::new(RwLock::new(None)),
|
|
2084
2318
|
symbol_index: Arc::new(RwLock::new(HashMap::new())),
|
|
2085
2319
|
symbol_refresh: Arc::new(Mutex::new(())),
|
|
2320
|
+
client_work_done_progress: Arc::new(RwLock::new(false)),
|
|
2086
2321
|
});
|
|
2087
2322
|
service
|
|
2088
2323
|
}
|
|
@@ -2337,6 +2572,83 @@ mod jsonrpc_integration_tests {
|
|
|
2337
2572
|
|
|
2338
2573
|
std::fs::remove_dir_all(&dir).ok();
|
|
2339
2574
|
}
|
|
2575
|
+
|
|
2576
|
+
fn did_change_watched_files(uris: &[&str]) -> Request {
|
|
2577
|
+
// FileChangeType::CHANGED == 2 in the LSP wire encoding.
|
|
2578
|
+
let changes: Vec<serde_json::Value> = uris
|
|
2579
|
+
.iter()
|
|
2580
|
+
.map(|u| serde_json::json!({ "uri": u, "type": 2 }))
|
|
2581
|
+
.collect();
|
|
2582
|
+
Request::build("workspace/didChangeWatchedFiles")
|
|
2583
|
+
.params(serde_json::json!({ "changes": changes }))
|
|
2584
|
+
.finish()
|
|
2585
|
+
}
|
|
2586
|
+
|
|
2587
|
+
// #164: with `window.workDoneProgress` advertised, the server must offer work-done progress on
|
|
2588
|
+
// workspace/symbol so the client sends a `workDoneToken` the handler can report/cancel against.
|
|
2589
|
+
#[tokio::test]
|
|
2590
|
+
async fn initialize_advertises_workspace_symbol_work_done_progress() {
|
|
2591
|
+
let mut service = new_service();
|
|
2592
|
+
let init = Request::build("initialize")
|
|
2593
|
+
.id(1)
|
|
2594
|
+
.params(serde_json::json!({
|
|
2595
|
+
"capabilities": { "window": { "workDoneProgress": true } }
|
|
2596
|
+
}))
|
|
2597
|
+
.finish();
|
|
2598
|
+
let result = call(&mut service, init).await.expect("initialize must return a result");
|
|
2599
|
+
assert_eq!(
|
|
2600
|
+
result["capabilities"]["workspaceSymbolProvider"]["workDoneProgress"],
|
|
2601
|
+
serde_json::json!(true),
|
|
2602
|
+
"workspace/symbol must advertise workDoneProgress, got {}",
|
|
2603
|
+
result["capabilities"]["workspaceSymbolProvider"]
|
|
2604
|
+
);
|
|
2605
|
+
}
|
|
2606
|
+
|
|
2607
|
+
// #161 end-to-end: after a `.d.tish` is indexed, an on-disk edit delivered via
|
|
2608
|
+
// didChangeWatchedFiles must be reflected on the next workspace/symbol query — the handler
|
|
2609
|
+
// invalidates the cached parse so a fresh one is picked up without a restart. `.d.tish` carries
|
|
2610
|
+
// the `tish` extension, so this also proves declaration files are walked at all.
|
|
2611
|
+
#[tokio::test]
|
|
2612
|
+
async fn did_change_watched_files_refreshes_d_tish_declaration() {
|
|
2613
|
+
let mut service = new_service();
|
|
2614
|
+
initialize(&mut service).await;
|
|
2615
|
+
|
|
2616
|
+
let dir = crate::test_fs::unique_temp_dir("dtish_watch");
|
|
2617
|
+
let decl = dir.join("ambient.d.tish");
|
|
2618
|
+
std::fs::write(&decl, "declare fn oldAmbient(): void\n").unwrap();
|
|
2619
|
+
let folder_uri = Url::from_file_path(&dir).unwrap().to_string();
|
|
2620
|
+
let file_uri = Url::from_file_path(&decl).unwrap().to_string();
|
|
2621
|
+
|
|
2622
|
+
// Register the folder and index the initial declaration.
|
|
2623
|
+
let _ = call(&mut service, did_change_workspace_folders(&[&folder_uri], &[])).await;
|
|
2624
|
+
let before = call(&mut service, symbol_request("oldAmbient")).await.expect("symbol result");
|
|
2625
|
+
assert_eq!(
|
|
2626
|
+
before.as_array().unwrap().len(),
|
|
2627
|
+
1,
|
|
2628
|
+
"the declaration in the .d.tish is indexed, got {before}"
|
|
2629
|
+
);
|
|
2630
|
+
|
|
2631
|
+
// Rewrite the declaration file on disk (distinct mtime) and notify via the watcher.
|
|
2632
|
+
std::thread::sleep(std::time::Duration::from_millis(20));
|
|
2633
|
+
std::fs::write(&decl, "declare fn newAmbient(): void\n").unwrap();
|
|
2634
|
+
let _ = call(&mut service, did_change_watched_files(&[&file_uri])).await;
|
|
2635
|
+
|
|
2636
|
+
// The new declaration is now searchable and the old one is gone — proof the watched-file
|
|
2637
|
+
// change forced a re-parse rather than serving the stale cached symbols.
|
|
2638
|
+
let new_hit = call(&mut service, symbol_request("newAmbient")).await.expect("symbol result");
|
|
2639
|
+
assert_eq!(
|
|
2640
|
+
new_hit.as_array().unwrap().len(),
|
|
2641
|
+
1,
|
|
2642
|
+
"the changed .d.tish declaration is re-indexed, got {new_hit}"
|
|
2643
|
+
);
|
|
2644
|
+
let old_gone = call(&mut service, symbol_request("oldAmbient")).await.expect("symbol result");
|
|
2645
|
+
assert!(
|
|
2646
|
+
old_gone.as_array().unwrap().is_empty(),
|
|
2647
|
+
"the stale declaration must be gone after the watched-file change, got {old_gone}"
|
|
2648
|
+
);
|
|
2649
|
+
|
|
2650
|
+
std::fs::remove_dir_all(&dir).ok();
|
|
2651
|
+
}
|
|
2340
2652
|
}
|
|
2341
2653
|
|
|
2342
2654
|
#[cfg(test)]
|
|
@@ -2428,6 +2740,20 @@ mod workspace_symbol_tests {
|
|
|
2428
2740
|
syms.iter().map(|s| s.name.as_str()).collect()
|
|
2429
2741
|
}
|
|
2430
2742
|
|
|
2743
|
+
/// Run a (never-cancelled) walk and unwrap the completed symbol list. Panics if the walk somehow
|
|
2744
|
+
/// reports cancellation, which cannot happen with an always-false flag.
|
|
2745
|
+
fn query(
|
|
2746
|
+
roots: &[PathBuf],
|
|
2747
|
+
index: &RwLock<HashMap<PathBuf, CachedFile>>,
|
|
2748
|
+
q: &str,
|
|
2749
|
+
) -> Vec<SymbolInformation> {
|
|
2750
|
+
let never = AtomicBool::new(false);
|
|
2751
|
+
match refresh_and_query_symbols(roots, index, q, &never) {
|
|
2752
|
+
WalkOutcome::Completed(s) => s,
|
|
2753
|
+
WalkOutcome::Cancelled => panic!("uncancelled walk must complete"),
|
|
2754
|
+
}
|
|
2755
|
+
}
|
|
2756
|
+
|
|
2431
2757
|
#[test]
|
|
2432
2758
|
fn indexes_queries_and_prunes_heavy_dirs() {
|
|
2433
2759
|
let dir = unique_temp_dir("idx");
|
|
@@ -2441,11 +2767,11 @@ mod workspace_symbol_tests {
|
|
|
2441
2767
|
let index = RwLock::new(HashMap::new());
|
|
2442
2768
|
let roots = [dir.clone()];
|
|
2443
2769
|
|
|
2444
|
-
let alpha =
|
|
2770
|
+
let alpha = query(&roots, &index, "alpha");
|
|
2445
2771
|
assert_eq!(names(&alpha), ["alphaFn"], "alphaFn matched; alphaDep pruned under node_modules");
|
|
2446
2772
|
// case-insensitive substring across kinds
|
|
2447
|
-
assert_eq!(names(&
|
|
2448
|
-
assert_eq!(names(&
|
|
2773
|
+
assert_eq!(names(&query(&roots, &index, "beta")), ["betaVar"]);
|
|
2774
|
+
assert_eq!(names(&query(&roots, &index, "gamma")), ["GammaType"]);
|
|
2449
2775
|
// The cache holds exactly the two real workspace files, not the node_modules one.
|
|
2450
2776
|
assert_eq!(index.read().unwrap().len(), 2, "two .tish files indexed, node_modules pruned");
|
|
2451
2777
|
|
|
@@ -2460,23 +2786,47 @@ mod workspace_symbol_tests {
|
|
|
2460
2786
|
let index = RwLock::new(HashMap::new());
|
|
2461
2787
|
let roots = [dir.clone()];
|
|
2462
2788
|
|
|
2463
|
-
assert_eq!(
|
|
2464
|
-
assert_eq!(
|
|
2789
|
+
assert_eq!(query(&roots, &index, "first").len(), 1);
|
|
2790
|
+
assert_eq!(query(&roots, &index, "second").len(), 0);
|
|
2465
2791
|
|
|
2466
2792
|
// Rewrite with new content; the newer mtime must invalidate the cached parse. A short sleep
|
|
2467
2793
|
// guarantees a distinct mtime even on coarse-resolution clocks.
|
|
2468
2794
|
std::thread::sleep(std::time::Duration::from_millis(20));
|
|
2469
2795
|
std::fs::write(&f, "fn second() {}\n").unwrap();
|
|
2470
|
-
assert_eq!(
|
|
2471
|
-
assert_eq!(
|
|
2796
|
+
assert_eq!(query(&roots, &index, "second").len(), 1, "re-parsed after edit");
|
|
2797
|
+
assert_eq!(query(&roots, &index, "first").len(), 0, "stale symbol gone");
|
|
2472
2798
|
|
|
2473
2799
|
// Deleting the file must evict it from the index.
|
|
2474
2800
|
std::fs::remove_file(&f).unwrap();
|
|
2475
|
-
assert_eq!(
|
|
2801
|
+
assert_eq!(query(&roots, &index, "second").len(), 0, "deleted file evicted");
|
|
2476
2802
|
assert!(index.read().unwrap().is_empty(), "index empty after the only file is removed");
|
|
2477
2803
|
|
|
2478
2804
|
std::fs::remove_dir_all(&dir).ok();
|
|
2479
2805
|
}
|
|
2806
|
+
|
|
2807
|
+
// #164: a pre-set cancel flag makes the walk bail before it scans/evicts, returning
|
|
2808
|
+
// `Cancelled` — the signal `symbol` turns into a `RequestCancelled` response instead of a
|
|
2809
|
+
// partial list, and (proven here) it leaves the shared index untouched.
|
|
2810
|
+
#[test]
|
|
2811
|
+
fn cancelled_walk_bails_without_mutating_index() {
|
|
2812
|
+
let dir = unique_temp_dir("cancel");
|
|
2813
|
+
std::fs::write(dir.join("c.tish"), "fn deltaSym() { return 1 }\n").unwrap();
|
|
2814
|
+
let index = RwLock::new(HashMap::new());
|
|
2815
|
+
let roots = [dir.clone()];
|
|
2816
|
+
|
|
2817
|
+
let cancelled = AtomicBool::new(true);
|
|
2818
|
+
let outcome = refresh_and_query_symbols(&roots, &index, "delta", &cancelled);
|
|
2819
|
+
assert!(
|
|
2820
|
+
matches!(outcome, WalkOutcome::Cancelled),
|
|
2821
|
+
"a pre-cancelled walk must report Cancelled"
|
|
2822
|
+
);
|
|
2823
|
+
assert!(
|
|
2824
|
+
index.read().unwrap().is_empty(),
|
|
2825
|
+
"a cancelled walk must not populate the index"
|
|
2826
|
+
);
|
|
2827
|
+
|
|
2828
|
+
std::fs::remove_dir_all(&dir).ok();
|
|
2829
|
+
}
|
|
2480
2830
|
}
|
|
2481
2831
|
|
|
2482
2832
|
#[cfg(test)]
|
|
@@ -294,9 +294,22 @@ tishlang_runtime = {{ path = {:?}{} }}
|
|
|
294
294
|
};
|
|
295
295
|
tishlang_build_utils::copy_binary_to_output(&artifact, &target)?;
|
|
296
296
|
|
|
297
|
+
cleanup_build_dir(&build_dir);
|
|
297
298
|
Ok(())
|
|
298
299
|
}
|
|
299
300
|
|
|
301
|
+
/// Remove a finished build's per-PID source directory (#384). Called only on SUCCESS, after the
|
|
302
|
+
/// binary is copied out — the dir is then disposable (it holds only the generated crate source;
|
|
303
|
+
/// compiled artifacts live in the shared workspace `target/`). Left in place on any earlier error so a
|
|
304
|
+
/// failed build can be inspected, and preserved entirely when `TISH_KEEP_BUILD_DIR=1` (e.g. to read the
|
|
305
|
+
/// generated `main.rs`). Best-effort: a failed removal is ignored.
|
|
306
|
+
pub(crate) fn cleanup_build_dir(build_dir: &Path) {
|
|
307
|
+
if std::env::var("TISH_KEEP_BUILD_DIR").as_deref() == Ok("1") {
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
let _ = std::fs::remove_dir_all(build_dir);
|
|
311
|
+
}
|
|
312
|
+
|
|
300
313
|
/// Build several native binaries in **one** nested Cargo project (shared `tishlang_runtime` compile).
|
|
301
314
|
///
|
|
302
315
|
/// `bins` order must match `outputs`: each `(stem, rust_code, generated_native_rs)` pairs with
|
|
@@ -454,6 +467,7 @@ tishlang_runtime = {{ path = {:?}{} }}
|
|
|
454
467
|
tishlang_build_utils::copy_binary_to_output(&binary, &target)?;
|
|
455
468
|
}
|
|
456
469
|
|
|
470
|
+
cleanup_build_dir(&build_dir);
|
|
457
471
|
Ok(())
|
|
458
472
|
}
|
|
459
473
|
|
|
@@ -461,6 +475,33 @@ tishlang_runtime = {{ path = {:?}{} }}
|
|
|
461
475
|
mod tests {
|
|
462
476
|
use super::runtime_features_for_cargo;
|
|
463
477
|
|
|
478
|
+
/// #384: `cleanup_build_dir` removes a finished build's dir by default, and preserves it under
|
|
479
|
+
/// `TISH_KEEP_BUILD_DIR=1`.
|
|
480
|
+
#[test]
|
|
481
|
+
fn cleanup_build_dir_removes_by_default_and_keeps_on_flag() {
|
|
482
|
+
use std::path::PathBuf;
|
|
483
|
+
// Scratch under the workspace target/ (never /tmp), unique per case.
|
|
484
|
+
let base: PathBuf = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
|
485
|
+
.join("../../target/tmp_cleanup_test");
|
|
486
|
+
let removed = base.join("to_remove");
|
|
487
|
+
let kept = base.join("to_keep");
|
|
488
|
+
for d in [&removed, &kept] {
|
|
489
|
+
std::fs::create_dir_all(d.join("src")).unwrap();
|
|
490
|
+
std::fs::write(d.join("src/main.rs"), "fn main(){}").unwrap();
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
std::env::remove_var("TISH_KEEP_BUILD_DIR");
|
|
494
|
+
super::cleanup_build_dir(&removed);
|
|
495
|
+
assert!(!removed.exists(), "default should remove the build dir");
|
|
496
|
+
|
|
497
|
+
std::env::set_var("TISH_KEEP_BUILD_DIR", "1");
|
|
498
|
+
super::cleanup_build_dir(&kept);
|
|
499
|
+
assert!(kept.exists(), "TISH_KEEP_BUILD_DIR=1 should preserve the build dir");
|
|
500
|
+
std::env::remove_var("TISH_KEEP_BUILD_DIR");
|
|
501
|
+
|
|
502
|
+
let _ = std::fs::remove_dir_all(&base);
|
|
503
|
+
}
|
|
504
|
+
|
|
464
505
|
#[test]
|
|
465
506
|
fn runtime_features_full_expands() {
|
|
466
507
|
let f = runtime_features_for_cargo(&["full".to_string()]);
|
|
@@ -9,3 +9,7 @@ repository = { workspace = true }
|
|
|
9
9
|
[dependencies]
|
|
10
10
|
tishlang_lexer = { path = "../tish_lexer", version = ">=0.1" }
|
|
11
11
|
tishlang_ast = { path = "../tish_ast", version = ">=0.1" }
|
|
12
|
+
# #381: grow the stack on demand during the recursive descent so deeply-nested source can't overflow
|
|
13
|
+
# the fixed OS thread stack before the MAX_PARSE_DEPTH bound converts it into a catchable error.
|
|
14
|
+
# (No-op shim on wasm32, matching tish_eval / tish_vm.)
|
|
15
|
+
stacker = "0.1"
|