+2
-1
.gitignore
+2
-1
.gitignore
+43
-1
Cargo.lock
+43
-1
Cargo.lock
···
408
408
]
409
409
410
410
[[package]]
411
+
name = "bstr"
412
+
version = "1.12.1"
413
+
source = "registry+https://github.com/rust-lang/crates.io-index"
414
+
checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab"
415
+
dependencies = [
416
+
"memchr",
417
+
"serde",
418
+
]
419
+
420
+
[[package]]
411
421
name = "btree-range-map"
412
422
version = "0.7.2"
413
423
source = "registry+https://github.com/rust-lang/crates.io-index"
···
708
718
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
709
719
710
720
[[package]]
721
+
name = "compile-time"
722
+
version = "0.2.0"
723
+
source = "registry+https://github.com/rust-lang/crates.io-index"
724
+
checksum = "e55ede5279d4d7c528906853743abeb26353ae1e6c440fcd6d18316c2c2dd903"
725
+
dependencies = [
726
+
"once_cell",
727
+
"proc-macro2",
728
+
"quote",
729
+
"rustc_version",
730
+
"semver",
731
+
"time",
732
+
]
733
+
734
+
[[package]]
711
735
name = "compression-codecs"
712
736
version = "0.4.35"
713
737
source = "registry+https://github.com/rust-lang/crates.io-index"
···
1372
1396
"ansi_term",
1373
1397
"anyhow",
1374
1398
"async-lock",
1399
+
"compile-time",
1375
1400
"console_error_panic_hook",
1376
1401
"futures",
1377
1402
"getrandom 0.3.4",
···
1384
1409
"nu-cmd-lang",
1385
1410
"nu-command",
1386
1411
"nu-engine",
1412
+
"nu-glob",
1387
1413
"nu-parser",
1388
1414
"nu-path",
1389
1415
"nu-protocol",
1390
1416
"rapidhash",
1391
1417
"reqwest",
1418
+
"rust-embed",
1392
1419
"scc",
1393
1420
"serde",
1394
1421
"serde_ipld_dagcbor",
···
1717
1744
version = "0.3.3"
1718
1745
source = "registry+https://github.com/rust-lang/crates.io-index"
1719
1746
checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
1747
+
1748
+
[[package]]
1749
+
name = "globset"
1750
+
version = "0.4.18"
1751
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1752
+
checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3"
1753
+
dependencies = [
1754
+
"aho-corasick",
1755
+
"bstr",
1756
+
"log",
1757
+
"regex-automata",
1758
+
"regex-syntax",
1759
+
]
1720
1760
1721
1761
[[package]]
1722
1762
name = "gloo-storage"
···
4392
4432
source = "registry+https://github.com/rust-lang/crates.io-index"
4393
4433
checksum = "60b161f275cb337fe0a44d924a5f4df0ed69c2c39519858f931ce61c779d3475"
4394
4434
dependencies = [
4435
+
"globset",
4395
4436
"sha2",
4396
4437
"walkdir",
4397
4438
]
···
5769
5810
[[package]]
5770
5811
name = "vfs"
5771
5812
version = "0.12.1"
5772
-
source = "git+https://github.com/landaire/rust-vfs?branch=fix%2Fwasm#c4341e8e7c16a019c1a1415fc8a413bf883a08d5"
5813
+
source = "git+https://github.com/90-008/rust-vfs?branch=fix%2Fwasm#547b30641d8f329614fb29e44c1c7360ef57ded9"
5773
5814
dependencies = [
5774
5815
"filetime",
5816
+
"rust-embed",
5775
5817
]
5776
5818
5777
5819
[[package]]
+5
-2
Cargo.toml
+5
-2
Cargo.toml
···
11
11
wasm-bindgen-futures = "0.4"
12
12
getrandom = { version = "0.3", features = ["wasm_js"] }
13
13
web-sys = { version = "0.3", features = ["console", "Window"] }
14
-
vfs = { version = "0.12" }
14
+
vfs = { version = "0.12", features = ["embedded-fs"] }
15
15
nu-command = { git = "https://github.com/90-008/nushell", default-features = false }
16
16
nu-engine = { git = "https://github.com/90-008/nushell", default-features = false }
17
17
nu-parser = { git = "https://github.com/90-008/nushell", default-features = false }
18
18
nu-protocol = { git = "https://github.com/90-008/nushell", default-features = false }
19
19
nu-path = { git = "https://github.com/90-008/nushell", default-features = false }
20
+
nu-glob = { git = "https://github.com/90-008/nushell", default-features = false }
20
21
nu-cmd-base = { git = "https://github.com/90-008/nushell", default-features = false }
21
22
nu-cmd-lang = { git = "https://github.com/90-008/nushell", default-features = false }
22
23
nu-cmd-extra = { git = "https://github.com/90-008/nushell", default-features = false }
···
38
39
scc = "3"
39
40
rapidhash = { version = "4", features = ["unsafe"] }
40
41
async-lock = "3.4.1"
42
+
compile-time = "0.2.0"
43
+
rust-embed = { version = "8.9.0", features = ["debug-embed", "include-exclude"] }
41
44
42
45
[patch.crates-io]
43
-
vfs = { git = "https://github.com/landaire/rust-vfs", branch = "fix/wasm" }
46
+
vfs = { git = "https://github.com/90-008/rust-vfs", branch = "fix/wasm" }
44
47
45
48
[profile.release]
46
49
opt-level = 3
+5
README.md
+5
README.md
···
1
1
nushell, but on the web, with a virtual environment for working in with various commands to interact with the environment, trying to emulate the nushell CLI from native OSes. faux + nu = faunu.
2
2
3
3
see [dysnomia.ptr.pet](https://dysnomia.ptr.pet) to try it out.
4
+
5
+
# TODOS
6
+
7
+
- add a separate worker for running commands in
8
+
- add more commands
+10
embedded/.dys/routine/access.nu
+10
embedded/.dys/routine/access.nu
···
1
+
def "dys access-log" [] {
2
+
let time = $env.LOGIN_TIME
3
+
print $'
4
+
/dysnomia.v000 /user: 90008/ /ip: [REDACTED]/ /time: [REDACTED]//
5
+
/dysnomia.v002 /user: 90008/ /ip: [REDACTED]/ /time: [REDACTED]//
6
+
/dysnomia.v011 /user: 90008/ /ip: [REDACTED]/ /time: [REDACTED]//
7
+
[...ENTRIES TRUNCATED...]
8
+
/dysnomia.v099 /user: anonymous/ /ip: [REDACTED]/ /time: ($time)//
9
+
'
10
+
}
+4
embedded/.dys/routine/version.nu
+4
embedded/.dys/routine/version.nu
+16
embedded/.dys/routine/welcome.nu
+16
embedded/.dys/routine/welcome.nu
+5
embedded/.dys/startup.nu
+5
embedded/.dys/startup.nu
+1
embedded/.env.nu
+1
embedded/.env.nu
···
1
+
. .dys/startup.nu
embedded/.gitkeep
embedded/.gitkeep
This is a binary file and will not be displayed.
+1
-1
flake.nix
+1
-1
flake.nix
···
23
23
nodejs-slim_latest deno
24
24
nodePackages.svelte-language-server
25
25
nodePackages.typescript-language-server
26
-
rustc rust-analyzer cargo wasm-pack wasm-bindgen-cli lld rustfmt binaryen
26
+
rustc rust-analyzer cargo wasm-pack wasm-bindgen-cli_0_2_104 lld rustfmt binaryen
27
27
];
28
28
shellHook = ''
29
29
export PATH="$PATH:$PWD/node_modules/.bin"
+5
-3
nix/wasm.nix
+5
-3
nix/wasm.nix
···
3
3
lib,
4
4
wasm-pack,
5
5
binaryen,
6
-
wasm-bindgen-cli,
6
+
wasm-bindgen-cli_0_2_104,
7
7
lld,
8
8
stdenv,
9
9
...
···
21
21
../Cargo.lock
22
22
../src
23
23
../.cargo
24
+
../embedded
24
25
];
25
26
};
26
27
27
28
cargoLock = {
28
29
lockFile = ../Cargo.lock;
29
30
outputHashes = {
30
-
"vfs-0.12.1" = "sha256-d249sIYhICdqqb7uoTRyhXAZTCF5zgjfItM4DE7b/gQ=";
31
+
"vfs-0.12.1" = "sha256-arpgwVsBhnn/2qawTR+NeyWRJOipr0kafg7VaiISufM=";
31
32
"jacquard-0.9.4" = "sha256-TEu4coueWzzkmFCkGb610Xrly7n8LUGMa5tdde/OElg=";
33
+
"nu-cmd-base-0.109.2" = "sha256-Q+6PxSmeiV/K6QP0I9xCiqZM37+p+CRLs7LMBUWurPo=";
32
34
};
33
35
};
34
36
35
-
nativeBuildInputs = [wasm-pack wasm-bindgen-cli lld];
37
+
nativeBuildInputs = [wasm-pack wasm-bindgen-cli_0_2_104 lld];
36
38
37
39
phases = ["unpackPhase" "buildPhase"];
38
40
+7
-3
src/cmd/cd.rs
+7
-3
src/cmd/cd.rs
···
1
-
use crate::globals::{get_pwd, get_vfs, set_pwd, to_shell_err};
1
+
use crate::{
2
+
error::to_shell_err,
3
+
globals::{get_pwd, get_vfs, set_pwd},
4
+
};
2
5
use nu_engine::CallExt;
3
6
use nu_protocol::{
4
-
Category, IntoValue, PipelineData, ShellError, Signature, SyntaxShape,
7
+
Category, IntoValue, PipelineData, ShellError, Signature, SyntaxShape, Type,
5
8
engine::{Command, EngineState, Stack},
6
9
};
7
10
use std::sync::Arc;
···
17
20
18
21
fn signature(&self) -> Signature {
19
22
Signature::build("cd")
20
-
.optional("path", SyntaxShape::String, "the path to change into")
23
+
.optional("path", SyntaxShape::Filepath, "the path to change into")
24
+
.input_output_type(Type::Nothing, Type::Nothing)
21
25
.category(Category::FileSystem)
22
26
}
23
27
+61
src/cmd/eval.rs
+61
src/cmd/eval.rs
···
1
+
use crate::globals::print_to_console;
2
+
use nu_engine::CallExt;
3
+
use nu_protocol::engine::Call;
4
+
use nu_protocol::{
5
+
Category, PipelineData, ShellError, Signature,
6
+
engine::{Command, EngineState, Stack},
7
+
};
8
+
use nu_protocol::{SyntaxShape, Type};
9
+
10
+
#[derive(Clone)]
11
+
pub struct Eval;
12
+
13
+
impl Command for Eval {
14
+
fn name(&self) -> &str {
15
+
"eval"
16
+
}
17
+
18
+
fn signature(&self) -> Signature {
19
+
Signature::build(self.name())
20
+
.optional("code", SyntaxShape::String, "code to evaluate")
21
+
.input_output_type(Type::one_of([Type::Nothing, Type::String]), Type::Nothing)
22
+
.category(Category::FileSystem)
23
+
}
24
+
25
+
fn description(&self) -> &str {
26
+
"evaluates a string as nushell code."
27
+
}
28
+
29
+
fn run(
30
+
&self,
31
+
engine_state: &EngineState,
32
+
stack: &mut Stack,
33
+
call: &Call,
34
+
input: PipelineData,
35
+
) -> Result<PipelineData, ShellError> {
36
+
let code: Option<String> = call.opt(engine_state, stack, 0)?;
37
+
38
+
let (span, code) = match code {
39
+
Some(c) => (Some(call.arguments_span()), c),
40
+
None => (
41
+
input.span(),
42
+
input.collect_string("\n", &engine_state.config)?,
43
+
),
44
+
};
45
+
46
+
match super::source_file::eval(engine_state, stack, &code, None) {
47
+
Ok(d) => Ok(d),
48
+
Err(err) => {
49
+
let msg: String = err.into();
50
+
print_to_console(&msg, true);
51
+
Err(ShellError::GenericError {
52
+
error: "source error".into(),
53
+
msg: "can't source string".into(),
54
+
span,
55
+
help: None,
56
+
inner: vec![],
57
+
})
58
+
}
59
+
}
60
+
}
61
+
}
+4
-2
src/cmd/fetch.rs
+4
-2
src/cmd/fetch.rs
···
1
-
use crate::globals::{get_pwd, print_to_console, register_task, remove_task, to_shell_err};
1
+
use crate::error::to_shell_err;
2
+
use crate::globals::{get_pwd, print_to_console, register_task, remove_task};
2
3
use anyhow::{Result, anyhow};
3
4
use futures::future::{AbortHandle, Abortable};
4
5
use jacquard::types::aturi::AtUri;
···
20
21
storage::{BlockStore, MemoryBlockStore},
21
22
};
22
23
use nu_engine::CallExt;
23
-
use nu_protocol::IntoPipelineData;
24
24
use nu_protocol::{
25
25
Category, PipelineData, ShellError, Signature, SyntaxShape, Value,
26
26
engine::{Command, EngineState, Stack},
27
27
};
28
+
use nu_protocol::{IntoPipelineData, Type};
28
29
use std::io::Write;
29
30
use std::str::FromStr;
30
31
use std::sync::Arc;
···
47
48
"HTTP URI or AT URI (at://identifier[/collection[/rkey]])",
48
49
)
49
50
.named("output", SyntaxShape::Filepath, "output path", Some('o'))
51
+
.input_output_type(Type::Nothing, Type::Nothing)
50
52
.category(Category::Network)
51
53
}
52
54
+313
src/cmd/glob.rs
+313
src/cmd/glob.rs
···
1
+
use std::sync::Arc;
2
+
3
+
use crate::globals::{get_pwd, get_vfs};
4
+
use nu_engine::CallExt;
5
+
use nu_glob::Pattern;
6
+
use nu_protocol::{
7
+
Category, ListStream, PipelineData, ShellError, Signature, SyntaxShape, Type, Value,
8
+
engine::{Command, EngineState, Stack},
9
+
};
10
+
use vfs::VfsFileType;
11
+
12
+
/// Options for glob matching
13
+
pub struct GlobOptions {
14
+
pub max_depth: Option<usize>,
15
+
pub no_dirs: bool,
16
+
pub no_files: bool,
17
+
}
18
+
19
+
impl Default for GlobOptions {
20
+
fn default() -> Self {
21
+
Self {
22
+
max_depth: None,
23
+
no_dirs: false,
24
+
no_files: false,
25
+
}
26
+
}
27
+
}
28
+
29
+
/// Expand a path (glob pattern or regular path) into a list of matching paths.
30
+
/// If the path is not a glob pattern, returns a single-item list.
31
+
/// Returns a vector of relative paths (relative to the base path).
32
+
pub fn expand_path(
33
+
path_str: &str,
34
+
base_path: Arc<vfs::VfsPath>,
35
+
options: GlobOptions,
36
+
) -> Result<Vec<String>, ShellError> {
37
+
// Check if it's a glob pattern
38
+
let is_glob = path_str.contains('*')
39
+
|| path_str.contains('?')
40
+
|| path_str.contains('[')
41
+
|| path_str.contains("**");
42
+
43
+
if is_glob {
44
+
glob_match(path_str, base_path, options)
45
+
} else {
46
+
// Single path: return as single-item list
47
+
Ok(vec![path_str.trim_start_matches('/').to_string()])
48
+
}
49
+
}
50
+
51
+
/// Match files and directories using a glob pattern.
52
+
/// Returns a vector of relative paths (relative to the base path) that match the pattern.
53
+
pub fn glob_match(
54
+
pattern_str: &str,
55
+
base_path: Arc<vfs::VfsPath>,
56
+
options: GlobOptions,
57
+
) -> Result<Vec<String>, ShellError> {
58
+
if pattern_str.is_empty() {
59
+
return Err(ShellError::GenericError {
60
+
error: "glob pattern must not be empty".into(),
61
+
msg: "glob pattern is empty".into(),
62
+
span: None,
63
+
help: Some("add characters to the glob pattern".into()),
64
+
inner: vec![],
65
+
});
66
+
}
67
+
68
+
// Parse the pattern
69
+
let pattern = Pattern::new(pattern_str).map_err(|e| ShellError::GenericError {
70
+
error: "error with glob pattern".into(),
71
+
msg: format!("{}", e),
72
+
span: None,
73
+
help: None,
74
+
inner: vec![],
75
+
})?;
76
+
77
+
// Determine max depth
78
+
let max_depth = if let Some(d) = options.max_depth {
79
+
d
80
+
} else if pattern_str.contains("**") {
81
+
usize::MAX
82
+
} else {
83
+
// Count number of / in pattern to determine depth
84
+
pattern_str.split('/').count()
85
+
};
86
+
87
+
// Normalize pattern: remove leading / for relative matching
88
+
let normalized_pattern = pattern_str.trim_start_matches('/');
89
+
let is_recursive = normalized_pattern.contains("**");
90
+
91
+
// Collect matching paths
92
+
let mut matches = Vec::new();
93
+
94
+
fn walk_directory(
95
+
current_path: Arc<vfs::VfsPath>,
96
+
current_relative_path: String,
97
+
pattern: &Pattern,
98
+
normalized_pattern: &str,
99
+
current_depth: usize,
100
+
max_depth: usize,
101
+
matches: &mut Vec<String>,
102
+
no_dirs: bool,
103
+
no_files: bool,
104
+
is_recursive: bool,
105
+
) -> Result<(), ShellError> {
106
+
if current_depth > max_depth {
107
+
return Ok(());
108
+
}
109
+
110
+
// Walk through directory entries
111
+
if let Ok(entries) = current_path.read_dir() {
112
+
for entry in entries {
113
+
let filename = entry.filename();
114
+
let entry_path =
115
+
current_path
116
+
.join(&filename)
117
+
.map_err(|e| ShellError::GenericError {
118
+
error: "path error".into(),
119
+
msg: e.to_string(),
120
+
span: None,
121
+
help: None,
122
+
inner: vec![],
123
+
})?;
124
+
125
+
// Build relative path from base
126
+
let new_relative = if current_relative_path.is_empty() {
127
+
filename.clone()
128
+
} else {
129
+
format!("{}/{}", current_relative_path, filename)
130
+
};
131
+
132
+
let metadata = entry_path
133
+
.metadata()
134
+
.map_err(|e| ShellError::GenericError {
135
+
error: "path error".into(),
136
+
msg: e.to_string(),
137
+
span: None,
138
+
help: None,
139
+
inner: vec![],
140
+
})?;
141
+
142
+
// Check if this path matches the pattern
143
+
// For patterns without path separators, match just the filename
144
+
// For patterns with path separators, match the full relative path
145
+
let path_to_match = if normalized_pattern.contains('/') {
146
+
&new_relative
147
+
} else {
148
+
&filename
149
+
};
150
+
151
+
if pattern.matches(path_to_match) {
152
+
let should_include = match metadata.file_type {
153
+
VfsFileType::Directory => !no_dirs,
154
+
VfsFileType::File => !no_files,
155
+
};
156
+
if should_include {
157
+
matches.push(new_relative.clone());
158
+
}
159
+
}
160
+
161
+
// Recursively walk into subdirectories
162
+
if metadata.file_type == VfsFileType::Directory {
163
+
// Only recurse if:
164
+
// 1. Pattern contains ** (recursive wildcard), OR
165
+
// 2. Pattern has path separators and we haven't matched all components yet
166
+
let has_path_separator = normalized_pattern.contains('/');
167
+
let pattern_component_count = if has_path_separator {
168
+
normalized_pattern.split('/').count()
169
+
} else {
170
+
1
171
+
};
172
+
173
+
let should_recurse = is_recursive
174
+
|| (has_path_separator && current_depth + 1 < pattern_component_count);
175
+
176
+
if should_recurse {
177
+
walk_directory(
178
+
Arc::new(entry_path),
179
+
new_relative,
180
+
pattern,
181
+
normalized_pattern,
182
+
current_depth + 1,
183
+
max_depth,
184
+
matches,
185
+
no_dirs,
186
+
no_files,
187
+
is_recursive,
188
+
)?;
189
+
}
190
+
}
191
+
}
192
+
}
193
+
194
+
Ok(())
195
+
}
196
+
197
+
// Start walking from base path
198
+
walk_directory(
199
+
base_path,
200
+
String::new(),
201
+
&pattern,
202
+
normalized_pattern,
203
+
0,
204
+
max_depth,
205
+
&mut matches,
206
+
options.no_dirs,
207
+
options.no_files,
208
+
is_recursive,
209
+
)?;
210
+
211
+
Ok(matches)
212
+
}
213
+
214
+
#[derive(Clone)]
215
+
pub struct Glob;
216
+
217
+
impl Command for Glob {
218
+
fn name(&self) -> &str {
219
+
"glob"
220
+
}
221
+
222
+
fn signature(&self) -> Signature {
223
+
Signature::build("glob")
224
+
.required(
225
+
"pattern",
226
+
SyntaxShape::OneOf(vec![SyntaxShape::String, SyntaxShape::GlobPattern]),
227
+
"the glob expression.",
228
+
)
229
+
.named(
230
+
"depth",
231
+
SyntaxShape::Int,
232
+
"directory depth to search",
233
+
Some('d'),
234
+
)
235
+
.switch(
236
+
"no-dir",
237
+
"whether to filter out directories from the returned paths",
238
+
Some('D'),
239
+
)
240
+
.switch(
241
+
"no-file",
242
+
"whether to filter out files from the returned paths",
243
+
Some('F'),
244
+
)
245
+
.input_output_type(Type::Nothing, Type::List(Box::new(Type::String)))
246
+
.category(Category::FileSystem)
247
+
}
248
+
249
+
fn description(&self) -> &str {
250
+
"creates a list of paths based on the glob pattern provided."
251
+
}
252
+
253
+
fn run(
254
+
&self,
255
+
engine_state: &EngineState,
256
+
stack: &mut Stack,
257
+
call: &nu_protocol::engine::Call,
258
+
_input: PipelineData,
259
+
) -> Result<PipelineData, ShellError> {
260
+
let span = call.head;
261
+
let pattern_value: Value = call.req(engine_state, stack, 0)?;
262
+
let pattern_span = pattern_value.span();
263
+
let depth: Option<i64> = call.get_flag(engine_state, stack, "depth")?;
264
+
let no_dirs = call.has_flag(engine_state, stack, "no-dir")?;
265
+
let no_files = call.has_flag(engine_state, stack, "no-file")?;
266
+
267
+
let pattern_str = match pattern_value {
268
+
Value::String { val, .. } | Value::Glob { val, .. } => val,
269
+
_ => {
270
+
return Err(ShellError::IncorrectValue {
271
+
msg: "incorrect glob pattern supplied to glob. use string or glob only."
272
+
.to_string(),
273
+
val_span: pattern_span,
274
+
call_span: pattern_span,
275
+
});
276
+
}
277
+
};
278
+
279
+
if pattern_str.is_empty() {
280
+
return Err(ShellError::GenericError {
281
+
error: "glob pattern must not be empty".into(),
282
+
msg: "glob pattern is empty".into(),
283
+
span: Some(pattern_span),
284
+
help: Some("add characters to the glob pattern".into()),
285
+
inner: vec![],
286
+
});
287
+
}
288
+
289
+
// Determine if pattern is absolute (starts with /)
290
+
let is_absolute = pattern_str.starts_with('/');
291
+
let base_path = if is_absolute { get_vfs() } else { get_pwd() };
292
+
293
+
// Use the glob_match function
294
+
let options = GlobOptions {
295
+
max_depth: depth.map(|d| d as usize),
296
+
no_dirs,
297
+
no_files,
298
+
};
299
+
300
+
let matches = glob_match(&pattern_str, base_path, options)?;
301
+
302
+
// Convert matches to Value stream
303
+
let signals = engine_state.signals().clone();
304
+
let values = matches
305
+
.into_iter()
306
+
.map(move |path| Value::string(path, span));
307
+
308
+
Ok(PipelineData::list_stream(
309
+
ListStream::new(values, span, signals.clone()),
310
+
None,
311
+
))
312
+
}
313
+
}
+2
-1
src/cmd/job_kill.rs
+2
-1
src/cmd/job_kill.rs
···
1
1
use crate::globals::kill_task_by_id;
2
2
use nu_engine::CallExt;
3
3
use nu_protocol::{
4
-
Category, IntoPipelineData, PipelineData, ShellError, Signature, SyntaxShape, Value,
4
+
Category, IntoPipelineData, PipelineData, ShellError, Signature, SyntaxShape, Type, Value,
5
5
engine::{Call, Command, EngineState, Stack},
6
6
};
7
7
···
16
16
fn signature(&self) -> Signature {
17
17
Signature::build("job kill")
18
18
.required("id", SyntaxShape::Int, "id of job to kill")
19
+
.input_output_type(Type::Nothing, Type::Nothing)
19
20
.category(Category::System)
20
21
}
21
22
+4
-2
src/cmd/job_list.rs
+4
-2
src/cmd/job_list.rs
···
1
1
use crate::globals::get_all_tasks;
2
2
use nu_protocol::{
3
-
Category, ListStream, PipelineData, Record, ShellError, Signature, Value,
3
+
Category, ListStream, PipelineData, Record, ShellError, Signature, Type, Value,
4
4
engine::{Call, Command, EngineState, Stack},
5
5
};
6
6
···
13
13
}
14
14
15
15
fn signature(&self) -> Signature {
16
-
Signature::build("job list").category(Category::System)
16
+
Signature::build("job list")
17
+
.input_output_type(Type::Nothing, Type::table())
18
+
.category(Category::System)
17
19
}
18
20
19
21
fn description(&self) -> &str {
+105
-45
src/cmd/ls.rs
+105
-45
src/cmd/ls.rs
···
1
-
use std::{
2
-
borrow::Cow,
3
-
sync::Arc,
4
-
time::{SystemTime, UNIX_EPOCH},
1
+
use std::time::{SystemTime, UNIX_EPOCH};
2
+
3
+
use crate::{
4
+
cmd::glob::{GlobOptions, expand_path},
5
+
error::to_shell_err,
6
+
globals::{get_pwd, get_vfs},
5
7
};
6
-
7
-
use crate::globals::{get_pwd, to_shell_err};
8
8
use jacquard::chrono;
9
9
use nu_engine::CallExt;
10
10
use nu_protocol::{
11
-
Category, ListStream, PipelineData, Record, ShellError, Signature, SyntaxShape, Value,
11
+
Category, ListStream, PipelineData, Record, ShellError, Signature, SyntaxShape, Type, Value,
12
12
engine::{Command, EngineState, Stack},
13
13
};
14
+
use std::sync::Arc;
14
15
15
16
#[derive(Clone)]
16
17
pub struct Ls;
···
22
23
23
24
fn signature(&self) -> Signature {
24
25
Signature::build("ls")
25
-
.optional("path", SyntaxShape::String, "the path to list")
26
+
.optional(
27
+
"path",
28
+
SyntaxShape::OneOf(vec![SyntaxShape::Filepath, SyntaxShape::GlobPattern]),
29
+
"the path to list",
30
+
)
26
31
.switch(
27
32
"all",
28
33
"include hidden paths (that start with a dot)",
···
34
39
Some('l'),
35
40
)
36
41
.switch("full-paths", "display paths as absolute paths", Some('f'))
42
+
.input_output_type(Type::Nothing, Type::table())
37
43
.category(Category::FileSystem)
38
44
}
39
45
···
48
54
call: &nu_protocol::engine::Call,
49
55
_input: PipelineData,
50
56
) -> Result<PipelineData, ShellError> {
51
-
let path_arg: Option<String> = call.opt(engine_state, stack, 0)?;
57
+
let path_arg: Option<Value> = call.opt(engine_state, stack, 0)?;
52
58
let all = call.has_flag(engine_state, stack, "all")?;
53
59
let long = call.has_flag(engine_state, stack, "long")?;
54
60
let full_paths = call.has_flag(engine_state, stack, "full-paths")?;
55
61
56
62
let pwd = get_pwd();
57
-
let mut target_dir = pwd.clone();
58
-
if let Some(path) = path_arg {
59
-
target_dir = Arc::new(
60
-
target_dir
61
-
.join(path.trim_end_matches('/'))
62
-
.map_err(to_shell_err(call.arguments_span()))?,
63
-
);
64
-
}
63
+
let span = call.head;
64
+
65
+
// If no path provided, list current directory
66
+
let (matches, base_path) = if let Some(path_val) = &path_arg {
67
+
let path_str = match path_val {
68
+
Value::String { val, .. } | Value::Glob { val, .. } => val,
69
+
_ => {
70
+
return Err(ShellError::GenericError {
71
+
error: "invalid path".into(),
72
+
msg: "path must be a string or glob pattern".into(),
73
+
span: Some(call.arguments_span()),
74
+
help: None,
75
+
inner: vec![],
76
+
});
77
+
}
78
+
};
79
+
80
+
let is_absolute = path_str.starts_with('/');
81
+
let base_path: Arc<vfs::VfsPath> = if is_absolute { get_vfs() } else { pwd.clone() };
82
+
83
+
// Check if it's a glob pattern
84
+
let is_glob = path_str.contains('*')
85
+
|| path_str.contains('?')
86
+
|| path_str.contains('[')
87
+
|| path_str.contains("**");
65
88
66
-
let span = call.head;
67
-
let entries = target_dir.read_dir().map_err(to_shell_err(span))?;
89
+
if is_glob {
90
+
// Glob pattern: expand and list matching paths
91
+
let options = GlobOptions {
92
+
max_depth: None,
93
+
no_dirs: false,
94
+
no_files: false,
95
+
};
96
+
let matches = expand_path(path_str, base_path.clone(), options)?;
97
+
(matches, base_path)
98
+
} else {
99
+
// Non-glob path: check if it's a directory and list its contents
100
+
let normalized_path = path_str.trim_start_matches('/').trim_end_matches('/');
101
+
let target_path = base_path
102
+
.join(normalized_path)
103
+
.map_err(to_shell_err(call.arguments_span()))?;
68
104
69
-
let make_record = move |name: &str, metadata: &vfs::VfsMetadata| {
105
+
let metadata = target_path.metadata().map_err(to_shell_err(span))?;
106
+
match metadata.file_type {
107
+
vfs::VfsFileType::Directory => {
108
+
// List directory contents
109
+
let entries = target_path.read_dir().map_err(to_shell_err(span))?;
110
+
let matches: Vec<String> = entries
111
+
.map(|e| {
112
+
// Build relative path from base_path
113
+
let entry_name = e.filename();
114
+
if normalized_path.is_empty() || normalized_path == "." {
115
+
entry_name
116
+
} else {
117
+
format!("{}/{}", normalized_path, entry_name)
118
+
}
119
+
})
120
+
.collect();
121
+
(matches, base_path)
122
+
}
123
+
vfs::VfsFileType::File => {
124
+
// Single file: return just this file (normalized, relative to base_path)
125
+
(vec![normalized_path.to_string()], base_path)
126
+
}
127
+
}
128
+
}
129
+
} else {
130
+
// No path: list current directory entries
131
+
let entries = pwd.read_dir().map_err(to_shell_err(span))?;
132
+
let matches: Vec<String> = entries.map(|e| e.filename()).collect();
133
+
(matches, pwd.clone())
134
+
};
135
+
136
+
let make_record = move |rel_path: &str| {
137
+
let full_path = base_path.join(rel_path).map_err(to_shell_err(span))?;
138
+
let metadata = full_path.metadata().map_err(to_shell_err(span))?;
139
+
140
+
// Filter hidden files if --all is not set
141
+
let filename = rel_path.split('/').last().unwrap_or(rel_path);
142
+
if filename.starts_with('.') && !all {
143
+
return Ok(None);
144
+
}
145
+
70
146
let type_str = match metadata.file_type {
71
147
vfs::VfsFileType::Directory => "dir",
72
148
vfs::VfsFileType::File => "file",
73
149
};
74
150
75
151
let mut record = Record::new();
76
-
record.push("name", Value::string(name, span));
152
+
let display_name = if full_paths {
153
+
full_path.as_str().to_string()
154
+
} else {
155
+
rel_path.to_string()
156
+
};
157
+
record.push("name", Value::string(display_name, span));
77
158
record.push("type", Value::string(type_str, span));
78
159
record.push("size", Value::filesize(metadata.len as i64, span));
79
160
let mut add_timestamp = |field: &str, timestamp: Option<SystemTime>| {
···
95
176
add_timestamp("created", metadata.created);
96
177
add_timestamp("accessed", metadata.accessed);
97
178
}
98
-
Value::record(record, span)
179
+
Ok(Some(Value::record(record, span)))
99
180
};
100
181
101
-
let entries = entries.into_iter().flat_map(move |entry| {
102
-
let do_map = || {
103
-
let name = entry.filename();
104
-
if name.starts_with('.') && !all {
105
-
return Ok(None);
106
-
}
107
-
let metadata = entry.metadata().map_err(to_shell_err(span))?;
108
-
109
-
let name = if full_paths {
110
-
format!("{path}/{name}", path = target_dir.as_str())
111
-
} else {
112
-
let path = target_dir
113
-
.as_str()
114
-
.trim_start_matches(pwd.as_str())
115
-
.trim_start_matches("/");
116
-
format!(
117
-
"{path}{sep}{name}",
118
-
sep = path.is_empty().then_some("").unwrap_or("/"),
119
-
)
120
-
};
121
-
Ok(Some(make_record(&name, &metadata)))
122
-
};
123
-
do_map()
182
+
let entries = matches.into_iter().flat_map(move |rel_path| {
183
+
make_record(&rel_path)
124
184
.transpose()
125
185
.map(|res| res.unwrap_or_else(|err| Value::error(err, span)))
126
186
});
+4
-3
src/cmd/mkdir.rs
+4
-3
src/cmd/mkdir.rs
···
1
-
use crate::globals::{get_pwd, to_shell_err};
1
+
use crate::{error::to_shell_err, globals::get_pwd};
2
2
use nu_engine::CallExt;
3
3
use nu_protocol::{
4
-
Category, PipelineData, ShellError, Signature, SyntaxShape,
4
+
Category, PipelineData, ShellError, Signature, SyntaxShape, Type,
5
5
engine::{Command, EngineState, Stack},
6
6
};
7
7
···
17
17
Signature::build("mkdir")
18
18
.required(
19
19
"path",
20
-
SyntaxShape::String,
20
+
SyntaxShape::Filepath,
21
21
"path of the directory(s) to create",
22
22
)
23
+
.input_output_type(Type::Nothing, Type::Nothing)
23
24
.category(Category::FileSystem)
24
25
}
25
26
+10
-4
src/cmd/mod.rs
+10
-4
src/cmd/mod.rs
···
1
1
pub mod cd;
2
+
pub mod eval;
2
3
pub mod fetch;
4
+
pub mod glob;
3
5
pub mod job;
4
6
pub mod job_kill;
5
7
pub mod job_list;
6
8
pub mod ls;
7
9
pub mod mkdir;
10
+
pub mod mv;
8
11
pub mod open;
12
+
pub mod print;
9
13
pub mod pwd;
10
14
pub mod random;
11
15
pub mod rm;
12
16
pub mod save;
13
-
pub mod source;
17
+
pub mod source_file;
14
18
pub mod sys;
15
-
pub mod version;
16
19
17
20
pub use cd::Cd;
21
+
pub use eval::Eval;
18
22
pub use fetch::Fetch;
23
+
pub use glob::Glob;
19
24
pub use job::Job;
20
25
pub use job_kill::JobKill;
21
26
pub use job_list::JobList;
22
27
pub use ls::Ls;
23
28
pub use mkdir::Mkdir;
29
+
pub use mv::Mv;
24
30
pub use open::Open;
31
+
pub use print::Print;
25
32
pub use pwd::Pwd;
26
33
pub use random::Random;
27
34
pub use rm::Rm;
28
35
pub use save::Save;
29
-
pub use source::Source;
36
+
pub use source_file::SourceFile;
30
37
pub use sys::Sys;
31
-
pub use version::Version;
+210
src/cmd/mv.rs
+210
src/cmd/mv.rs
···
1
+
use std::io::{Read, Write};
2
+
3
+
use crate::{
4
+
cmd::glob::{GlobOptions, expand_path},
5
+
error::to_shell_err,
6
+
globals::{get_pwd, get_vfs},
7
+
};
8
+
use nu_engine::CallExt;
9
+
use nu_protocol::{
10
+
Category, PipelineData, ShellError, Signature, SyntaxShape, Type, Value,
11
+
engine::{Command, EngineState, Stack},
12
+
};
13
+
use std::sync::Arc;
14
+
use vfs::{VfsError, VfsFileType};
15
+
16
+
#[derive(Clone)]
17
+
pub struct Mv;
18
+
19
+
impl Command for Mv {
20
+
fn name(&self) -> &str {
21
+
"mv"
22
+
}
23
+
24
+
fn signature(&self) -> Signature {
25
+
Signature::build("mv")
26
+
.required(
27
+
"source",
28
+
SyntaxShape::OneOf(vec![SyntaxShape::Filepath, SyntaxShape::GlobPattern]),
29
+
"path to the file or directory to move",
30
+
)
31
+
.required(
32
+
"destination",
33
+
SyntaxShape::Filepath,
34
+
"path to the destination",
35
+
)
36
+
.input_output_type(Type::Nothing, Type::Nothing)
37
+
.category(Category::FileSystem)
38
+
}
39
+
40
+
fn description(&self) -> &str {
41
+
"move a file or directory in the virtual filesystem."
42
+
}
43
+
44
+
fn run(
45
+
&self,
46
+
engine_state: &EngineState,
47
+
stack: &mut Stack,
48
+
call: &nu_protocol::engine::Call,
49
+
_input: PipelineData,
50
+
) -> Result<PipelineData, ShellError> {
51
+
let source_value: Value = call.req(engine_state, stack, 0)?;
52
+
let dest_path: String = call.req(engine_state, stack, 1)?;
53
+
54
+
let source_str = match source_value {
55
+
Value::String { val, .. } | Value::Glob { val, .. } => val,
56
+
_ => {
57
+
return Err(ShellError::GenericError {
58
+
error: "invalid source path".into(),
59
+
msg: "source must be a string or glob pattern".into(),
60
+
span: Some(call.arguments_span()),
61
+
help: None,
62
+
inner: vec![],
63
+
});
64
+
}
65
+
};
66
+
67
+
// Prevent moving root
68
+
if source_str == "/" {
69
+
return Err(ShellError::GenericError {
70
+
error: "cannot move root".to_string(),
71
+
msg: "refusing to move root directory".to_string(),
72
+
span: Some(call.arguments_span()),
73
+
help: None,
74
+
inner: vec![],
75
+
});
76
+
}
77
+
78
+
// Expand source path (glob or single) into list of paths
79
+
let is_absolute = source_str.starts_with('/');
80
+
let base_path: Arc<vfs::VfsPath> = if is_absolute { get_vfs() } else { get_pwd() };
81
+
82
+
let options = GlobOptions {
83
+
max_depth: None,
84
+
no_dirs: false,
85
+
no_files: false,
86
+
};
87
+
88
+
let matches = expand_path(&source_str, base_path.clone(), options)?;
89
+
let is_glob = matches.len() > 1
90
+
|| source_str.contains('*')
91
+
|| source_str.contains('?')
92
+
|| source_str.contains('[')
93
+
|| source_str.contains("**");
94
+
95
+
// Resolve destination
96
+
let dest = get_pwd()
97
+
.join(dest_path.trim_end_matches('/'))
98
+
.map_err(to_shell_err(call.arguments_span()))?;
99
+
100
+
// For glob patterns, destination must be a directory
101
+
if is_glob {
102
+
let dest_meta = dest
103
+
.metadata()
104
+
.map_err(to_shell_err(call.arguments_span()))?;
105
+
if dest_meta.file_type != VfsFileType::Directory {
106
+
return Err(ShellError::GenericError {
107
+
error: "destination must be a directory".to_string(),
108
+
msg: "when using glob patterns, destination must be a directory".to_string(),
109
+
span: Some(call.arguments_span()),
110
+
help: None,
111
+
inner: vec![],
112
+
});
113
+
}
114
+
}
115
+
116
+
// Move each matching file/directory
117
+
for rel_path in matches {
118
+
let source = base_path
119
+
.join(&rel_path)
120
+
.map_err(to_shell_err(call.arguments_span()))?;
121
+
let source_meta = source
122
+
.metadata()
123
+
.map_err(to_shell_err(call.arguments_span()))?;
124
+
125
+
// Determine destination path
126
+
let dest_entry = if is_glob {
127
+
// For glob patterns, use filename in destination directory
128
+
let filename = rel_path.split('/').last().unwrap_or(&rel_path);
129
+
dest.join(filename)
130
+
.map_err(to_shell_err(call.arguments_span()))?
131
+
} else {
132
+
// For single path, use destination as-is
133
+
dest.clone()
134
+
};
135
+
136
+
match source_meta.file_type {
137
+
VfsFileType::File => move_file(&source, &dest_entry, call.arguments_span())?,
138
+
VfsFileType::Directory => {
139
+
move_directory(&source, &dest_entry, call.arguments_span())?
140
+
}
141
+
}
142
+
}
143
+
144
+
Ok(PipelineData::Empty)
145
+
}
146
+
}
147
+
148
+
fn move_file(
149
+
source: &vfs::VfsPath,
150
+
dest: &vfs::VfsPath,
151
+
span: nu_protocol::Span,
152
+
) -> Result<(), ShellError> {
153
+
// Read source file content
154
+
let mut source_file = source.open_file().map_err(to_shell_err(span))?;
155
+
156
+
let mut contents = Vec::new();
157
+
source_file
158
+
.read_to_end(&mut contents)
159
+
.map_err(|e| ShellError::GenericError {
160
+
error: "io error".to_string(),
161
+
msg: format!("failed to read source file: {}", e),
162
+
span: Some(span),
163
+
help: None,
164
+
inner: vec![],
165
+
})?;
166
+
167
+
// Create destination file and write content
168
+
dest.create_file()
169
+
.map_err(to_shell_err(span))
170
+
.and_then(|mut f| {
171
+
f.write_all(&contents)
172
+
.map_err(VfsError::from)
173
+
.map_err(to_shell_err(span))
174
+
})?;
175
+
176
+
// Remove source file
177
+
source.remove_file().map_err(to_shell_err(span))?;
178
+
179
+
Ok(())
180
+
}
181
+
182
+
fn move_directory(
183
+
source: &vfs::VfsPath,
184
+
dest: &vfs::VfsPath,
185
+
span: nu_protocol::Span,
186
+
) -> Result<(), ShellError> {
187
+
// Try to create destination directory (create_dir_all handles parent creation)
188
+
// If it already exists, that's fine - we'll move entries into it
189
+
let _ = dest.create_dir_all().map_err(to_shell_err(span));
190
+
191
+
// Recursively move all entries
192
+
let entries = source.read_dir().map_err(to_shell_err(span))?;
193
+
for entry_name in entries {
194
+
let source_entry = source
195
+
.join(entry_name.as_str())
196
+
.map_err(to_shell_err(span))?;
197
+
let dest_entry = dest.join(entry_name.as_str()).map_err(to_shell_err(span))?;
198
+
199
+
let entry_meta = source_entry.metadata().map_err(to_shell_err(span))?;
200
+
match entry_meta.file_type {
201
+
VfsFileType::File => move_file(&source_entry, &dest_entry, span)?,
202
+
VfsFileType::Directory => move_directory(&source_entry, &dest_entry, span)?,
203
+
}
204
+
}
205
+
206
+
// Remove source directory
207
+
source.remove_dir_all().map_err(to_shell_err(span))?;
208
+
209
+
Ok(())
210
+
}
+128
-30
src/cmd/open.rs
+128
-30
src/cmd/open.rs
···
1
1
use std::ops::Not;
2
2
3
-
use crate::globals::{get_pwd, to_shell_err};
3
+
use crate::{
4
+
cmd::glob::{GlobOptions, expand_path},
5
+
globals::{get_pwd, get_vfs},
6
+
};
4
7
use nu_command::{FromCsv, FromJson, FromOds, FromToml, FromTsv, FromXlsx, FromXml, FromYaml};
5
8
use nu_engine::CallExt;
6
9
use nu_protocol::{
7
-
ByteStream, Category, PipelineData, ShellError, Signature, SyntaxShape,
10
+
ByteStream, Category, ListStream, PipelineData, ShellError, Signature, SyntaxShape, Type,
11
+
Value,
8
12
engine::{Command, EngineState, Stack},
9
13
};
14
+
use std::sync::Arc;
10
15
11
16
#[derive(Clone)]
12
17
pub struct Open;
···
18
23
19
24
fn signature(&self) -> Signature {
20
25
Signature::build("open")
21
-
.required("path", SyntaxShape::String, "path to the file")
26
+
.required(
27
+
"path",
28
+
SyntaxShape::OneOf(vec![SyntaxShape::Filepath, SyntaxShape::GlobPattern]),
29
+
"path to the file",
30
+
)
22
31
.switch(
23
32
"raw",
24
33
"output content as raw string/binary without parsing",
25
34
Some('r'),
26
35
)
36
+
.input_output_type(Type::Nothing, Type::one_of([Type::String, Type::Binary]))
27
37
.category(Category::FileSystem)
28
38
}
29
39
···
38
48
call: &nu_protocol::engine::Call,
39
49
_input: PipelineData,
40
50
) -> Result<PipelineData, ShellError> {
41
-
let path: String = call.req(engine_state, stack, 0)?;
51
+
let path_value: Value = call.req(engine_state, stack, 0)?;
42
52
let raw_flag = call.has_flag(engine_state, stack, "raw")?;
43
53
44
-
let target_file = get_pwd().join(&path).map_err(to_shell_err(call.head))?;
54
+
let path_str = match path_value {
55
+
Value::String { val, .. } | Value::Glob { val, .. } => val,
56
+
_ => {
57
+
return Err(ShellError::GenericError {
58
+
error: "invalid path".into(),
59
+
msg: "path must be a string or glob pattern".into(),
60
+
span: Some(call.head),
61
+
help: None,
62
+
inner: vec![],
63
+
});
64
+
}
65
+
};
66
+
67
+
// Expand path (glob or single) into list of paths
68
+
let is_absolute = path_str.starts_with('/');
69
+
let base_path: Arc<vfs::VfsPath> = if is_absolute { get_vfs() } else { get_pwd() };
70
+
71
+
let options = GlobOptions {
72
+
max_depth: None,
73
+
no_dirs: true, // Only open files, not directories
74
+
no_files: false,
75
+
};
45
76
46
-
let parse_cmd = raw_flag
47
-
.not()
48
-
.then(|| {
49
-
target_file
50
-
.extension()
51
-
.and_then(|ext| get_cmd_for_ext(&ext))
52
-
})
53
-
.flatten();
77
+
let matches = expand_path(&path_str, base_path.clone(), options)?;
54
78
55
-
target_file
56
-
.open_file()
57
-
.map_err(to_shell_err(call.head))
58
-
.and_then(|f| {
59
-
let data = PipelineData::ByteStream(
60
-
ByteStream::read(
61
-
f,
62
-
call.head,
63
-
engine_state.signals().clone(),
64
-
nu_protocol::ByteStreamType::String,
65
-
),
66
-
None,
67
-
);
68
-
if let Some(cmd) = parse_cmd {
69
-
return cmd.run(engine_state, stack, call, data);
79
+
let span = call.head;
80
+
let signals = engine_state.signals().clone();
81
+
82
+
// Open each matching file
83
+
let mut results = Vec::new();
84
+
for rel_path in matches {
85
+
let target_file = match base_path.join(&rel_path) {
86
+
Ok(p) => p,
87
+
Err(e) => {
88
+
results.push(Value::error(
89
+
ShellError::GenericError {
90
+
error: "path error".into(),
91
+
msg: e.to_string(),
92
+
span: Some(span),
93
+
help: None,
94
+
inner: vec![],
95
+
},
96
+
span,
97
+
));
98
+
continue;
70
99
}
71
-
Ok(data)
72
-
})
100
+
};
101
+
102
+
let parse_cmd = raw_flag
103
+
.not()
104
+
.then(|| {
105
+
target_file
106
+
.extension()
107
+
.and_then(|ext| get_cmd_for_ext(&ext))
108
+
})
109
+
.flatten();
110
+
111
+
match target_file.open_file() {
112
+
Ok(f) => {
113
+
let data = PipelineData::ByteStream(
114
+
ByteStream::read(
115
+
f,
116
+
span,
117
+
signals.clone(),
118
+
nu_protocol::ByteStreamType::String,
119
+
),
120
+
None,
121
+
);
122
+
123
+
let value = if let Some(cmd) = parse_cmd {
124
+
match cmd.run(engine_state, stack, call, data) {
125
+
Ok(pipeline_data) => {
126
+
// Convert pipeline data to value
127
+
pipeline_data
128
+
.into_value(span)
129
+
.unwrap_or_else(|e| Value::error(e, span))
130
+
}
131
+
Err(e) => Value::error(e, span),
132
+
}
133
+
} else {
134
+
data.into_value(span)
135
+
.unwrap_or_else(|e| Value::error(e, span))
136
+
};
137
+
results.push(value);
138
+
}
139
+
Err(e) => {
140
+
results.push(Value::error(
141
+
ShellError::GenericError {
142
+
error: "io error".into(),
143
+
msg: format!("failed to open file {}: {}", rel_path, e),
144
+
span: Some(span),
145
+
help: None,
146
+
inner: vec![],
147
+
},
148
+
span,
149
+
));
150
+
}
151
+
}
152
+
}
153
+
154
+
// If single file, return the single result directly (for backward compatibility)
155
+
if results.len() == 1
156
+
&& !path_str.contains('*')
157
+
&& !path_str.contains('?')
158
+
&& !path_str.contains('[')
159
+
&& !path_str.contains("**")
160
+
{
161
+
match results.into_iter().next().unwrap() {
162
+
Value::Error { error, .. } => Err(*error),
163
+
val => Ok(PipelineData::Value(val, None)),
164
+
}
165
+
} else {
166
+
Ok(PipelineData::list_stream(
167
+
ListStream::new(results.into_iter(), span, signals.clone()),
168
+
None,
169
+
))
170
+
}
73
171
}
74
172
}
75
173
+46
src/cmd/print.rs
+46
src/cmd/print.rs
···
1
+
use crate::globals::print_to_console;
2
+
use nu_engine::CallExt;
3
+
use nu_protocol::{
4
+
Category, PipelineData, ShellError, Signature, SyntaxShape, Type, Value,
5
+
engine::{Command, EngineState, Stack},
6
+
};
7
+
8
+
#[derive(Clone)]
9
+
pub struct Print;
10
+
11
+
impl Command for Print {
12
+
fn name(&self) -> &str {
13
+
"print"
14
+
}
15
+
16
+
fn signature(&self) -> Signature {
17
+
Signature::build("print")
18
+
.rest("rest", SyntaxShape::Any, "values to print")
19
+
.input_output_type(Type::Nothing, Type::Nothing)
20
+
.category(Category::Strings)
21
+
}
22
+
23
+
fn description(&self) -> &str {
24
+
"print values to the console."
25
+
}
26
+
27
+
fn run(
28
+
&self,
29
+
engine_state: &EngineState,
30
+
stack: &mut Stack,
31
+
call: &nu_protocol::engine::Call,
32
+
_input: PipelineData,
33
+
) -> Result<PipelineData, ShellError> {
34
+
let rest: Vec<Value> = call.rest(engine_state, stack, 0)?;
35
+
36
+
let mut parts = Vec::new();
37
+
for value in rest {
38
+
let s = value.to_expanded_string(" ", &engine_state.config);
39
+
parts.push(s);
40
+
}
41
+
let output = parts.join(" ");
42
+
print_to_console(&output, true);
43
+
44
+
Ok(PipelineData::Empty)
45
+
}
46
+
}
+4
-1
src/cmd/pwd.rs
+4
-1
src/cmd/pwd.rs
···
1
1
use crate::globals::get_pwd;
2
+
use nu_protocol::Type;
2
3
use nu_protocol::engine::Call;
3
4
use nu_protocol::{
4
5
Category, IntoPipelineData, PipelineData, ShellError, Signature, Value,
···
14
15
}
15
16
16
17
fn signature(&self) -> Signature {
17
-
Signature::build("pwd").category(Category::FileSystem)
18
+
Signature::build("pwd")
19
+
.input_output_type(Type::Nothing, Type::String)
20
+
.category(Category::FileSystem)
18
21
}
19
22
20
23
fn description(&self) -> &str {
+53
-24
src/cmd/rm.rs
+53
-24
src/cmd/rm.rs
···
1
-
use crate::globals::{get_pwd, to_shell_err};
1
+
use crate::{
2
+
cmd::glob::{GlobOptions, expand_path},
3
+
error::to_shell_err,
4
+
globals::{get_pwd, get_vfs},
5
+
};
2
6
use nu_engine::CallExt;
3
7
use nu_protocol::{
4
-
Category, PipelineData, ShellError, Signature, SyntaxShape,
8
+
Category, PipelineData, ShellError, Signature, SyntaxShape, Type, Value,
5
9
engine::{Command, EngineState, Stack},
6
10
};
11
+
use std::sync::Arc;
7
12
use vfs::VfsFileType;
8
13
9
14
#[derive(Clone)]
···
18
23
Signature::build("rm")
19
24
.required(
20
25
"path",
21
-
SyntaxShape::String,
26
+
SyntaxShape::OneOf(vec![SyntaxShape::Filepath, SyntaxShape::GlobPattern]),
22
27
"path to file or directory to remove",
23
28
)
24
29
.switch(
···
26
31
"remove directories and their contents recursively",
27
32
Some('r'),
28
33
)
34
+
.input_output_type(Type::Nothing, Type::Nothing)
29
35
.category(Category::FileSystem)
30
36
}
31
37
···
40
46
call: &nu_protocol::engine::Call,
41
47
_input: PipelineData,
42
48
) -> Result<PipelineData, ShellError> {
43
-
let path: String = call.req(engine_state, stack, 0)?;
49
+
let path_value: Value = call.req(engine_state, stack, 0)?;
44
50
let recursive = call.has_flag(engine_state, stack, "recursive")?;
45
51
52
+
let path_str = match path_value {
53
+
Value::String { val, .. } | Value::Glob { val, .. } => val,
54
+
_ => {
55
+
return Err(ShellError::GenericError {
56
+
error: "invalid path".into(),
57
+
msg: "path must be a string or glob pattern".into(),
58
+
span: Some(call.head),
59
+
help: None,
60
+
inner: vec![],
61
+
});
62
+
}
63
+
};
64
+
46
65
// Prevent removing root
47
-
if path == "/" {
66
+
if path_str == "/" {
48
67
return Err(ShellError::GenericError {
49
68
error: "cannot remove root".to_string(),
50
69
msg: "refusing to remove root directory".to_string(),
···
54
73
});
55
74
}
56
75
57
-
// Resolve target relative to PWD (or absolute if path starts with '/')
58
-
let target = get_pwd()
59
-
.join(path.trim_end_matches('/'))
60
-
.map_err(to_shell_err(call.head))?;
76
+
// Expand path (glob or single) into list of paths
77
+
let is_absolute = path_str.starts_with('/');
78
+
let base_path: Arc<vfs::VfsPath> = if is_absolute { get_vfs() } else { get_pwd() };
79
+
80
+
let options = GlobOptions {
81
+
max_depth: None,
82
+
no_dirs: false,
83
+
no_files: false,
84
+
};
85
+
86
+
let matches = expand_path(&path_str, base_path.clone(), options)?;
61
87
62
-
let meta = target.metadata().map_err(to_shell_err(call.head))?;
63
-
match meta.file_type {
64
-
VfsFileType::File => {
65
-
target.remove_file().map_err(to_shell_err(call.head))?;
66
-
Ok(PipelineData::Empty)
67
-
}
68
-
VfsFileType::Directory => {
69
-
(if recursive {
70
-
target.remove_dir_all()
71
-
} else {
72
-
// non-recursive: attempt to remove directory (will fail if not empty)
73
-
target.remove_dir()
74
-
})
75
-
.map_err(to_shell_err(call.head))
76
-
.map(|_| PipelineData::Empty)
88
+
// Remove all matching paths
89
+
for rel_path in matches {
90
+
let target = base_path.join(&rel_path).map_err(to_shell_err(call.head))?;
91
+
let meta = target.metadata().map_err(to_shell_err(call.head))?;
92
+
match meta.file_type {
93
+
VfsFileType::File => {
94
+
target.remove_file().map_err(to_shell_err(call.head))?;
95
+
}
96
+
VfsFileType::Directory => {
97
+
(if recursive {
98
+
target.remove_dir_all()
99
+
} else {
100
+
target.remove_dir()
101
+
})
102
+
.map_err(to_shell_err(call.head))?;
103
+
}
77
104
}
78
105
}
106
+
107
+
Ok(PipelineData::Empty)
79
108
}
80
109
}
+2
-2
src/cmd/save.rs
+2
-2
src/cmd/save.rs
···
1
-
use crate::globals::{get_pwd, to_shell_err};
1
+
use crate::{error::to_shell_err, globals::get_pwd};
2
2
use nu_engine::CallExt;
3
3
use nu_protocol::{
4
4
Category, PipelineData, ShellError, Signature, SyntaxShape, Type, Value,
···
16
16
17
17
fn signature(&self) -> Signature {
18
18
Signature::build("save")
19
-
.required("path", SyntaxShape::String, "path to write the data to")
19
+
.required("path", SyntaxShape::Filepath, "path to write the data to")
20
20
.input_output_types(vec![(Type::Any, Type::Nothing)])
21
21
.category(Category::FileSystem)
22
22
}
-89
src/cmd/source.rs
-89
src/cmd/source.rs
···
1
-
use crate::globals::{get_pwd, queue_delta, to_shell_err};
2
-
use nu_engine::{CallExt, command_prelude::IoError, eval_block};
3
-
use nu_parser::parse;
4
-
use nu_protocol::{
5
-
Category, PipelineData, ShellError, Signature, SyntaxShape,
6
-
debugger::WithoutDebug,
7
-
engine::{Command, EngineState, Stack, StateWorkingSet},
8
-
};
9
-
use std::{io::Read, path::PathBuf, str::FromStr};
10
-
11
-
#[derive(Clone)]
12
-
pub struct Source;
13
-
14
-
impl Command for Source {
15
-
fn name(&self) -> &str {
16
-
"source"
17
-
}
18
-
19
-
fn signature(&self) -> Signature {
20
-
Signature::build(self.name())
21
-
.required("filename", SyntaxShape::String, "the file to source")
22
-
.category(Category::Core)
23
-
}
24
-
25
-
fn description(&self) -> &str {
26
-
"source a file from the virtual filesystem."
27
-
}
28
-
29
-
fn run(
30
-
&self,
31
-
engine_state: &EngineState,
32
-
stack: &mut Stack,
33
-
call: &nu_protocol::engine::Call,
34
-
input: PipelineData,
35
-
) -> Result<PipelineData, ShellError> {
36
-
let filename: String = call.req(engine_state, stack, 0)?;
37
-
38
-
// 1. Read file from VFS
39
-
let path = get_pwd().join(&filename).map_err(to_shell_err(call.head))?;
40
-
let mut file = path.open_file().map_err(to_shell_err(call.head))?;
41
-
let mut contents = String::new();
42
-
file.read_to_string(&mut contents).map_err(|e| {
43
-
ShellError::Io(IoError::new(
44
-
e,
45
-
call.head,
46
-
PathBuf::from_str(path.as_str()).unwrap(),
47
-
))
48
-
})?;
49
-
50
-
// 2. Parse the content
51
-
// We create a new working set based on the CURRENT engine state.
52
-
let mut working_set = StateWorkingSet::new(engine_state);
53
-
54
-
// We must add the file to the working set so the parser can track spans correctly
55
-
let _file_id = working_set.add_file(filename.clone(), contents.as_bytes());
56
-
57
-
// Parse the block
58
-
let block = parse(
59
-
&mut working_set,
60
-
Some(&filename),
61
-
contents.as_bytes(),
62
-
false,
63
-
);
64
-
65
-
if let Some(err) = working_set.parse_errors.first() {
66
-
return Err(ShellError::GenericError {
67
-
error: "Parse error".into(),
68
-
msg: err.to_string(),
69
-
span: Some(call.head),
70
-
help: None,
71
-
inner: vec![],
72
-
});
73
-
}
74
-
75
-
// 3. Prepare execution context
76
-
// We clone the engine state to merge the new definitions (delta) locally.
77
-
// This ensures the script can call its own defined functions immediately.
78
-
let mut local_state = engine_state.clone();
79
-
local_state.merge_delta(working_set.delta.clone())?;
80
-
81
-
// 4. Queue the delta for the global engine state
82
-
// This allows definitions to be available in the next command execution cycle (REPL behavior).
83
-
queue_delta(working_set.delta);
84
-
85
-
// 5. Evaluate the block
86
-
// We pass the MUTABLE stack, so environment variable changes (PWD, load-env) WILL persist.
87
-
eval_block::<WithoutDebug>(&local_state, stack, &block, input).map(|data| data.body)
88
-
}
89
-
}
+161
src/cmd/source_file.rs
+161
src/cmd/source_file.rs
···
1
+
use crate::{
2
+
cmd::glob::glob_match,
3
+
error::{CommandError, to_shell_err},
4
+
globals::{get_pwd, get_vfs, print_to_console, set_pwd},
5
+
};
6
+
use nu_engine::{CallExt, get_eval_block_with_early_return};
7
+
use nu_parser::parse;
8
+
use nu_protocol::{
9
+
Category, PipelineData, ShellError, Signature, SyntaxShape, Type, Value,
10
+
engine::{Command, EngineState, Stack, StateWorkingSet},
11
+
};
12
+
use std::sync::Arc;
13
+
14
+
#[derive(Clone)]
15
+
pub struct SourceFile;
16
+
17
+
impl Command for SourceFile {
18
+
fn name(&self) -> &str {
19
+
"eval file"
20
+
}
21
+
22
+
fn signature(&self) -> Signature {
23
+
Signature::build(self.name())
24
+
.required(
25
+
"path",
26
+
SyntaxShape::OneOf(vec![SyntaxShape::Filepath, SyntaxShape::GlobPattern]),
27
+
"the file to source",
28
+
)
29
+
.input_output_type(Type::Nothing, Type::Nothing)
30
+
.category(Category::Core)
31
+
}
32
+
33
+
fn description(&self) -> &str {
34
+
"sources a file from the virtual filesystem."
35
+
}
36
+
37
+
fn run(
38
+
&self,
39
+
engine_state: &EngineState,
40
+
stack: &mut Stack,
41
+
call: &nu_protocol::engine::Call,
42
+
_input: PipelineData,
43
+
) -> Result<PipelineData, ShellError> {
44
+
let span = call.arguments_span();
45
+
let path: Value = call.req(engine_state, stack, 0)?;
46
+
47
+
// Check if path is a glob pattern
48
+
let path_str = match &path {
49
+
Value::String { val, .. } | Value::Glob { val, .. } => val.clone(),
50
+
_ => {
51
+
return Err(ShellError::GenericError {
52
+
error: "not a path or glob pattern".into(),
53
+
msg: String::new(),
54
+
span: Some(span),
55
+
help: None,
56
+
inner: vec![],
57
+
});
58
+
}
59
+
};
60
+
61
+
let pwd = get_pwd();
62
+
let is_absolute = path_str.starts_with('/');
63
+
let base_path: Arc<vfs::VfsPath> = if is_absolute { get_vfs() } else { pwd.clone() };
64
+
65
+
// Check if it's a glob pattern (contains *, ?, [, or **)
66
+
let is_glob = path_str.contains('*')
67
+
|| path_str.contains('?')
68
+
|| path_str.contains('[')
69
+
|| path_str.contains("**");
70
+
71
+
let paths_to_source = if is_glob {
72
+
// Expand glob pattern
73
+
let options = crate::cmd::glob::GlobOptions {
74
+
max_depth: None,
75
+
no_dirs: true, // Only source files, not directories
76
+
no_files: false,
77
+
};
78
+
glob_match(&path_str, base_path.clone(), options)?
79
+
} else {
80
+
// Single file path
81
+
vec![path_str]
82
+
};
83
+
84
+
// Source each matching file
85
+
for rel_path in paths_to_source {
86
+
let full_path = base_path.join(&rel_path).map_err(to_shell_err(span))?;
87
+
88
+
let metadata = full_path.metadata().map_err(to_shell_err(span))?;
89
+
if metadata.file_type != vfs::VfsFileType::File {
90
+
continue;
91
+
}
92
+
93
+
let contents = full_path.read_to_string().map_err(to_shell_err(span))?;
94
+
95
+
set_pwd(full_path.parent().into());
96
+
let res = eval(engine_state, stack, &contents, Some(&full_path.filename()));
97
+
set_pwd(pwd.clone());
98
+
99
+
match res {
100
+
Ok(p) => {
101
+
print_to_console(&p.collect_string("\n", &engine_state.config)?, true);
102
+
}
103
+
Err(err) => {
104
+
let msg: String = err.into();
105
+
print_to_console(&msg, true);
106
+
return Err(ShellError::GenericError {
107
+
error: "source error".into(),
108
+
msg: format!("can't source file: {}", rel_path),
109
+
span: Some(span),
110
+
help: None,
111
+
inner: vec![],
112
+
});
113
+
}
114
+
}
115
+
}
116
+
117
+
Ok(PipelineData::Empty)
118
+
}
119
+
}
120
+
121
+
pub fn eval(
122
+
engine_state: &EngineState,
123
+
stack: &mut Stack,
124
+
contents: &str,
125
+
filename: Option<&str>,
126
+
) -> Result<PipelineData, CommandError> {
127
+
let filename = filename.unwrap_or("<piped data>");
128
+
let mut working_set = StateWorkingSet::new(engine_state);
129
+
let start_offset = working_set.next_span_start();
130
+
let _ = working_set.add_file(filename.into(), contents.as_bytes());
131
+
132
+
let block = parse(&mut working_set, Some(filename), contents.as_bytes(), false);
133
+
134
+
if let Some(err) = working_set.parse_errors.into_iter().next() {
135
+
web_sys::console::error_1(&err.to_string().into());
136
+
return Err(CommandError::new(err, contents).with_start_offset(start_offset));
137
+
}
138
+
if let Some(err) = working_set.compile_errors.into_iter().next() {
139
+
web_sys::console::error_1(&err.to_string().into());
140
+
return Err(CommandError::new(err, contents).with_start_offset(start_offset));
141
+
}
142
+
143
+
// uhhhhh this is safe prolly cuz we are single threaded
144
+
// i mean still shouldnt do this but i lowkey dont care so :3
145
+
let engine_state = unsafe {
146
+
std::ptr::from_ref(engine_state)
147
+
.cast_mut()
148
+
.as_mut()
149
+
.unwrap()
150
+
};
151
+
engine_state
152
+
.merge_delta(working_set.delta)
153
+
.map_err(|err| CommandError::new(err, contents).with_start_offset(start_offset))?;
154
+
155
+
// queue_delta(working_set.delta.clone());
156
+
157
+
let eval_block_with_early_return = get_eval_block_with_early_return(&engine_state);
158
+
eval_block_with_early_return(&engine_state, stack, &block, PipelineData::Empty)
159
+
.map(|d| d.body)
160
+
.map_err(|err| CommandError::new(err, contents).with_start_offset(start_offset))
161
+
}
+10
-1
src/cmd/sys.rs
+10
-1
src/cmd/sys.rs
···
1
1
use js_sys::Reflect;
2
2
use js_sys::global;
3
+
use nu_protocol::Type;
3
4
use nu_protocol::{
4
5
Category, IntoPipelineData, PipelineData, Record, ShellError, Signature, Value,
5
6
engine::{Command, EngineState, Stack},
···
15
16
}
16
17
17
18
fn signature(&self) -> Signature {
18
-
Signature::build("sys").category(Category::System)
19
+
Signature::build("sys")
20
+
.input_output_type(Type::Nothing, Type::record())
21
+
.category(Category::System)
19
22
}
20
23
21
24
fn description(&self) -> &str {
···
176
179
Value::string("not running in a browser environment", head),
177
180
);
178
181
}
182
+
183
+
let date = compile_time::unix!();
184
+
let rustc = compile_time::rustc_version_str!();
185
+
186
+
rec.push("build_time", Value::int(date, head));
187
+
rec.push("rustc_version", Value::string(rustc, head));
179
188
180
189
Ok(Value::record(rec, head).into_pipeline_data())
181
190
}
-32
src/cmd/version.rs
-32
src/cmd/version.rs
···
1
-
use nu_protocol::engine::Call;
2
-
use nu_protocol::{
3
-
Category, IntoPipelineData, PipelineData, ShellError, Signature, Value,
4
-
engine::{Command, EngineState, Stack},
5
-
};
6
-
7
-
#[derive(Clone)]
8
-
pub struct Version;
9
-
10
-
impl Command for Version {
11
-
fn name(&self) -> &str {
12
-
"version"
13
-
}
14
-
15
-
fn signature(&self) -> Signature {
16
-
Signature::build(self.name()).category(Category::System)
17
-
}
18
-
19
-
fn description(&self) -> &str {
20
-
"print the version of dysnomia."
21
-
}
22
-
23
-
fn run(
24
-
&self,
25
-
_engine_state: &EngineState,
26
-
_stack: &mut Stack,
27
-
call: &Call,
28
-
_input: PipelineData,
29
-
) -> Result<PipelineData, ShellError> {
30
-
Ok(Value::string("dysnomia.v099.t1765660500", call.head).into_pipeline_data())
31
-
}
32
-
}
+1163
src/completion/context.rs
+1163
src/completion/context.rs
···
1
+
use crate::completion::helpers::*;
2
+
use crate::completion::types::{CompletionContext, CompletionKind};
3
+
use crate::console_log;
4
+
use nu_parser::FlatShape;
5
+
use nu_protocol::engine::{EngineState, StateWorkingSet};
6
+
use nu_protocol::{Signature, Span};
7
+
8
+
pub fn find_command_and_arg_index(
9
+
input: &str,
10
+
shapes: &[(Span, FlatShape)],
11
+
current_idx: usize,
12
+
current_local_span: Span,
13
+
global_offset: usize,
14
+
) -> Option<(String, usize)> {
15
+
let mut command_name: Option<String> = None;
16
+
let mut arg_count = 0;
17
+
18
+
// Look backwards through shapes to find the command
19
+
for i in (0..current_idx).rev() {
20
+
if let Some((prev_span, prev_shape)) = shapes.get(i) {
21
+
let prev_local_span = to_local_span(*prev_span, global_offset);
22
+
23
+
// Check if there's a separator between this shape and the next one
24
+
let next_shape_start = if i + 1 < shapes.len() {
25
+
to_local_span(shapes[i + 1].0, global_offset).start
26
+
} else {
27
+
current_local_span.start
28
+
};
29
+
30
+
if has_separator_between(input, prev_local_span.end, next_shape_start) {
31
+
break; // Stop at separator
32
+
}
33
+
34
+
if is_command_shape(input, prev_shape, prev_local_span) {
35
+
// Found the command
36
+
let cmd_text = safe_slice(input, prev_local_span);
37
+
let cmd_name = extract_command_name(cmd_text);
38
+
command_name = Some(cmd_name.to_string());
39
+
break;
40
+
} else {
41
+
// This is an argument - count it if it's not a flag
42
+
let arg_text = safe_slice(input, prev_local_span);
43
+
let trimmed_arg = arg_text.trim();
44
+
// Don't count flags (starting with -) or empty arguments
45
+
if !trimmed_arg.is_empty() && !trimmed_arg.starts_with('-') {
46
+
arg_count += 1;
47
+
}
48
+
}
49
+
}
50
+
}
51
+
52
+
command_name.map(|name| (name, arg_count))
53
+
}
54
+
55
+
pub fn build_command_prefix(
56
+
input: &str,
57
+
shapes: &[(Span, FlatShape)],
58
+
current_idx: usize,
59
+
current_local_span: Span,
60
+
current_prefix: &str,
61
+
global_offset: usize,
62
+
) -> (String, Span) {
63
+
let mut span_start = current_local_span.start;
64
+
65
+
// Look backwards through shapes to find previous command words
66
+
for i in (0..current_idx).rev() {
67
+
if let Some((prev_span, prev_shape)) = shapes.get(i) {
68
+
let prev_local_span = to_local_span(*prev_span, global_offset);
69
+
70
+
if is_command_shape(input, prev_shape, prev_local_span) {
71
+
// Check if there's a separator between this shape and the next one
72
+
let next_shape_start = if i + 1 < shapes.len() {
73
+
to_local_span(shapes[i + 1].0, global_offset).start
74
+
} else {
75
+
current_local_span.start
76
+
};
77
+
78
+
// Check if there's a separator (pipe, semicolon, etc.) between shapes
79
+
// Whitespace is fine, but separators indicate a new command
80
+
if has_separator_between(input, prev_local_span.end, next_shape_start) {
81
+
break; // Stop at separator
82
+
}
83
+
84
+
// Update span start to include this command word
85
+
span_start = prev_local_span.start;
86
+
} else {
87
+
// Not a command shape, stop looking backwards
88
+
break;
89
+
}
90
+
}
91
+
}
92
+
93
+
// Extract the full prefix from the input, preserving exact spacing
94
+
let span_end = current_local_span.end;
95
+
let full_prefix = if span_start < input.len() {
96
+
safe_slice(input, Span::new(span_start, span_end)).to_string()
97
+
} else {
98
+
current_prefix.to_string()
99
+
};
100
+
101
+
(full_prefix, Span::new(span_start, span_end))
102
+
}
103
+
104
+
pub fn get_command_signature(engine_guard: &EngineState, cmd_name: &str) -> Option<Signature> {
105
+
engine_guard
106
+
.find_decl(cmd_name.as_bytes(), &[])
107
+
.map(|id| engine_guard.get_decl(id).signature())
108
+
}
109
+
110
+
/// Creates CommandArgument context(s), and optionally adds a Command context for subcommands
111
+
/// if we're at argument index 0 and the command has subcommands.
112
+
pub fn create_command_argument_contexts(
113
+
command_name: String,
114
+
arg_index: usize,
115
+
prefix: String,
116
+
span: Span,
117
+
working_set: &StateWorkingSet,
118
+
_engine_guard: &EngineState,
119
+
) -> Vec<CompletionContext> {
120
+
let mut contexts = Vec::new();
121
+
122
+
// Always add the CommandArgument context
123
+
contexts.push(CompletionContext {
124
+
kind: CompletionKind::CommandArgument {
125
+
command_name: command_name.clone(),
126
+
arg_index,
127
+
},
128
+
prefix: prefix.clone(),
129
+
span,
130
+
});
131
+
132
+
// If we're at argument index 0, check if the command has subcommands
133
+
if arg_index == 0 {
134
+
// Check if command has subcommands
135
+
// Subcommands are commands that start with "command_name " (with space)
136
+
let parent_prefix = format!("{} ", command_name);
137
+
let subcommands = working_set
138
+
.find_commands_by_predicate(|value| value.starts_with(parent_prefix.as_bytes()), true);
139
+
140
+
if !subcommands.is_empty() {
141
+
// Command has subcommands - add a Command context for subcommands
142
+
console_log!(
143
+
"[completion] Command {command_name:?} has subcommands, adding Command context for subcommands"
144
+
);
145
+
contexts.push(CompletionContext {
146
+
kind: CompletionKind::Command {
147
+
parent_command: Some(command_name),
148
+
},
149
+
prefix,
150
+
span,
151
+
});
152
+
}
153
+
}
154
+
155
+
contexts
156
+
}
157
+
158
+
pub fn determine_flag_or_argument_context(
159
+
input: &str,
160
+
shapes: &[(Span, FlatShape)],
161
+
prefix: &str,
162
+
idx: usize,
163
+
local_span: Span,
164
+
span: Span,
165
+
global_offset: usize,
166
+
working_set: &StateWorkingSet,
167
+
_engine_guard: &EngineState,
168
+
) -> Vec<CompletionContext> {
169
+
let trimmed_prefix = prefix.trim();
170
+
if trimmed_prefix.starts_with('-') {
171
+
// This looks like a flag - find the command
172
+
if let Some((cmd_name, _)) =
173
+
find_command_and_arg_index(input, shapes, idx, local_span, global_offset)
174
+
{
175
+
vec![CompletionContext {
176
+
kind: CompletionKind::Flag {
177
+
command_name: cmd_name,
178
+
},
179
+
prefix: trimmed_prefix.to_string(),
180
+
span,
181
+
}]
182
+
} else {
183
+
vec![CompletionContext {
184
+
kind: CompletionKind::Argument,
185
+
prefix: prefix.to_string(),
186
+
span,
187
+
}]
188
+
}
189
+
} else {
190
+
// This is a positional argument - find the command and argument index
191
+
if let Some((cmd_name, arg_index)) =
192
+
find_command_and_arg_index(input, shapes, idx, local_span, global_offset)
193
+
{
194
+
create_command_argument_contexts(
195
+
cmd_name,
196
+
arg_index,
197
+
trimmed_prefix.to_string(),
198
+
span,
199
+
working_set,
200
+
_engine_guard,
201
+
)
202
+
} else {
203
+
vec![CompletionContext {
204
+
kind: CompletionKind::Argument,
205
+
prefix: prefix.to_string(),
206
+
span,
207
+
}]
208
+
}
209
+
}
210
+
}
211
+
212
+
pub fn handle_block_or_closure(
213
+
input: &str,
214
+
shapes: &[(Span, FlatShape)],
215
+
working_set: &StateWorkingSet,
216
+
engine_guard: &EngineState,
217
+
prefix: &str,
218
+
span: Span,
219
+
shape_name: &str,
220
+
current_idx: usize,
221
+
local_span: Span,
222
+
global_offset: usize,
223
+
) -> Vec<CompletionContext> {
224
+
console_log!("[completion] Processing {shape_name} shape with prefix: {prefix:?}");
225
+
226
+
// Check if the content ends with a pipe or semicolon
227
+
let prefix_ends_with_separator = ends_with_separator(prefix);
228
+
let last_sep_pos_in_prefix = if prefix_ends_with_separator {
229
+
find_last_separator_pos(prefix)
230
+
} else {
231
+
None
232
+
};
233
+
console_log!(
234
+
"[completion] {shape_name}: prefix_ends_with_separator={prefix_ends_with_separator}, last_sep_pos_in_prefix={last_sep_pos_in_prefix:?}"
235
+
);
236
+
237
+
if let Some((trimmed_prefix, adjusted_span, is_empty)) = handle_block_prefix(prefix, span) {
238
+
console_log!(
239
+
"[completion] {shape_name}: trimmed_prefix={trimmed_prefix:?}, is_empty={is_empty}"
240
+
);
241
+
242
+
if is_empty {
243
+
// Empty block/closure or just whitespace
244
+
// Check if there's a command shape before this closure/block shape
245
+
// If so, we might be completing after that command
246
+
let mut found_command: Option<String> = None;
247
+
for i in (0..current_idx).rev() {
248
+
if let Some((prev_span, prev_shape)) = shapes.get(i) {
249
+
let prev_local_span = to_local_span(*prev_span, global_offset);
250
+
// Check if this shape is before the current closure and is a command
251
+
if prev_local_span.end <= local_span.start {
252
+
if is_command_shape(input, prev_shape, prev_local_span) {
253
+
let cmd_text = safe_slice(input, prev_local_span);
254
+
let cmd_full = cmd_text.trim().to_string();
255
+
256
+
// Extract the full command text - if it contains spaces, it might be a subcommand
257
+
// We'll use the first word for parent_command to show subcommands
258
+
// The suggestion generator will filter appropriately
259
+
let cmd_first_word = extract_command_name(cmd_text).to_string();
260
+
261
+
// If the command contains spaces, it's likely a full command (subcommand)
262
+
// In that case, we shouldn't show subcommands
263
+
if cmd_full.contains(' ') && cmd_full != cmd_first_word {
264
+
// It's a full command (subcommand), don't show subcommands
265
+
console_log!(
266
+
"[completion] {shape_name} is empty but found full command {cmd_full:?} before it, not showing completions"
267
+
);
268
+
return Vec::new();
269
+
}
270
+
271
+
// Use the first word to show subcommands
272
+
found_command = Some(cmd_first_word);
273
+
console_log!(
274
+
"[completion] {shape_name} is empty but found command {found_command:?} before it"
275
+
);
276
+
break;
277
+
}
278
+
}
279
+
}
280
+
}
281
+
282
+
if let Some(cmd_name) = found_command {
283
+
// We found a command before the closure, show subcommands of that command
284
+
console_log!(
285
+
"[completion] {shape_name} is empty, showing subcommands of {cmd_name:?}"
286
+
);
287
+
vec![CompletionContext {
288
+
kind: CompletionKind::Command {
289
+
parent_command: Some(cmd_name),
290
+
},
291
+
prefix: String::new(),
292
+
span: adjusted_span,
293
+
}]
294
+
} else {
295
+
// Truly empty - show all commands
296
+
console_log!("[completion] {shape_name} is empty, setting Command context");
297
+
vec![CompletionContext {
298
+
kind: CompletionKind::Command {
299
+
parent_command: None,
300
+
},
301
+
prefix: String::new(),
302
+
span: adjusted_span,
303
+
}]
304
+
}
305
+
} else if let Some(last_sep_pos) = last_sep_pos_in_prefix {
306
+
// After a separator - command context
307
+
let after_sep = prefix[last_sep_pos..].trim_start();
308
+
console_log!(
309
+
"[completion] {shape_name} has separator at {last_sep_pos}, after_sep={after_sep:?}, setting Command context"
310
+
);
311
+
vec![CompletionContext {
312
+
kind: CompletionKind::Command {
313
+
parent_command: None,
314
+
},
315
+
prefix: after_sep.to_string(),
316
+
span: Span::new(span.start + last_sep_pos, span.end),
317
+
}]
318
+
} else {
319
+
console_log!(
320
+
"[completion] {shape_name} has no separator, checking for variable/flag/argument context"
321
+
);
322
+
// Check if this is a variable or cell path first
323
+
let trimmed = trimmed_prefix.trim();
324
+
325
+
if trimmed.starts_with('$') {
326
+
// Variable or cell path completion
327
+
if let Some((var_name, path_so_far, cell_prefix)) = parse_cell_path(trimmed) {
328
+
let var_id = lookup_variable_id(var_name, working_set);
329
+
330
+
if let Some(var_id) = var_id {
331
+
let prefix_byte_len = cell_prefix.len();
332
+
let cell_span_start = adjusted_span.end.saturating_sub(prefix_byte_len);
333
+
console_log!(
334
+
"[completion] {shape_name}: Setting CellPath context with var {var_name:?}, prefix {cell_prefix:?}"
335
+
);
336
+
vec![CompletionContext {
337
+
kind: CompletionKind::CellPath {
338
+
var_id,
339
+
path_so_far: path_so_far.iter().map(|s| s.to_string()).collect(),
340
+
},
341
+
prefix: cell_prefix.to_string(),
342
+
span: Span::new(cell_span_start, adjusted_span.end),
343
+
}]
344
+
} else {
345
+
// Unknown variable, fall back to variable completion
346
+
let var_prefix = trimmed[1..].to_string();
347
+
console_log!(
348
+
"[completion] {shape_name}: Unknown var, setting Variable context with prefix {var_prefix:?}"
349
+
);
350
+
vec![CompletionContext {
351
+
kind: CompletionKind::Variable,
352
+
prefix: var_prefix,
353
+
span: adjusted_span,
354
+
}]
355
+
}
356
+
} else {
357
+
// Simple variable completion (no dot)
358
+
let var_prefix = if trimmed.len() > 1 {
359
+
trimmed[1..].to_string()
360
+
} else {
361
+
String::new()
362
+
};
363
+
console_log!(
364
+
"[completion] {shape_name}: Setting Variable context with prefix {var_prefix:?}"
365
+
);
366
+
vec![CompletionContext {
367
+
kind: CompletionKind::Variable,
368
+
prefix: var_prefix,
369
+
span: adjusted_span,
370
+
}]
371
+
}
372
+
} else if trimmed.starts_with('-') {
373
+
// Flag completion
374
+
if let Some((cmd_name, _)) = find_command_and_arg_index(
375
+
input,
376
+
shapes,
377
+
current_idx,
378
+
local_span,
379
+
global_offset,
380
+
) {
381
+
console_log!(
382
+
"[completion] {shape_name}: Found command {cmd_name:?} for flag completion"
383
+
);
384
+
vec![CompletionContext {
385
+
kind: CompletionKind::Flag {
386
+
command_name: cmd_name,
387
+
},
388
+
prefix: trimmed.to_string(),
389
+
span: adjusted_span,
390
+
}]
391
+
} else {
392
+
vec![CompletionContext {
393
+
kind: CompletionKind::Argument,
394
+
prefix: trimmed_prefix.to_string(),
395
+
span: adjusted_span,
396
+
}]
397
+
}
398
+
} else {
399
+
// Try to find the command and argument index
400
+
if let Some((cmd_name, arg_index)) = find_command_and_arg_index(
401
+
input,
402
+
shapes,
403
+
current_idx,
404
+
local_span,
405
+
global_offset,
406
+
) {
407
+
console_log!(
408
+
"[completion] {shape_name}: Found command {cmd_name:?} with arg_index {arg_index} for argument completion"
409
+
);
410
+
create_command_argument_contexts(
411
+
cmd_name,
412
+
arg_index,
413
+
trimmed.to_string(),
414
+
adjusted_span,
415
+
working_set,
416
+
engine_guard,
417
+
)
418
+
} else {
419
+
// No command found, treat as regular argument
420
+
console_log!(
421
+
"[completion] {shape_name}: No command found, using Argument context"
422
+
);
423
+
vec![CompletionContext {
424
+
kind: CompletionKind::Argument,
425
+
prefix: trimmed_prefix.to_string(),
426
+
span: adjusted_span,
427
+
}]
428
+
}
429
+
}
430
+
}
431
+
} else {
432
+
Vec::new()
433
+
}
434
+
}
435
+
436
+
pub fn handle_variable_string_shape(
437
+
input: &str,
438
+
shapes: &[(Span, FlatShape)],
439
+
working_set: &StateWorkingSet,
440
+
engine_guard: &EngineState,
441
+
idx: usize,
442
+
prefix: &str,
443
+
span: Span,
444
+
local_span: Span,
445
+
global_offset: usize,
446
+
) -> Vec<CompletionContext> {
447
+
if idx == 0 {
448
+
return Vec::new();
449
+
}
450
+
451
+
let prev_shape = &shapes[idx - 1];
452
+
let prev_local_span = to_local_span(prev_shape.0, global_offset);
453
+
454
+
if let FlatShape::Variable(var_id) = prev_shape.1 {
455
+
// Check if the variable shape ends right where this shape starts (or very close)
456
+
// Allow for a small gap (like a dot) between shapes
457
+
let gap = local_span.start.saturating_sub(prev_local_span.end);
458
+
if gap <= 1 {
459
+
// This is a cell path - the String shape contains the field name(s)
460
+
// The prefix might be like "na" or "field.subfield"
461
+
let trimmed_prefix = prefix.trim();
462
+
let (path_so_far, cell_prefix) = parse_cell_path_from_fields(trimmed_prefix);
463
+
464
+
let prefix_byte_len = cell_prefix.len();
465
+
let cell_span_start = span.end.saturating_sub(prefix_byte_len);
466
+
console_log!(
467
+
"[completion] Detected cell path from Variable+String shapes, var_id={var_id:?}, prefix={cell_prefix:?}, path={path_so_far:?}"
468
+
);
469
+
vec![CompletionContext {
470
+
kind: CompletionKind::CellPath {
471
+
var_id,
472
+
path_so_far: path_so_far.iter().map(|s| s.to_string()).collect(),
473
+
},
474
+
prefix: cell_prefix.to_string(),
475
+
span: Span::new(cell_span_start, span.end),
476
+
}]
477
+
} else {
478
+
// Gap between shapes, use helper to determine context
479
+
determine_flag_or_argument_context(
480
+
input,
481
+
shapes,
482
+
&prefix.trim(),
483
+
idx,
484
+
local_span,
485
+
span,
486
+
global_offset,
487
+
working_set,
488
+
engine_guard,
489
+
)
490
+
}
491
+
} else {
492
+
// Previous shape is not a Variable, use helper to determine context
493
+
determine_flag_or_argument_context(
494
+
input,
495
+
shapes,
496
+
&prefix.trim(),
497
+
idx,
498
+
local_span,
499
+
span,
500
+
global_offset,
501
+
working_set,
502
+
engine_guard,
503
+
)
504
+
}
505
+
}
506
+
507
+
pub fn handle_dot_shape(
508
+
_input: &str,
509
+
shapes: &[(Span, FlatShape)],
510
+
idx: usize,
511
+
prefix: &str,
512
+
span: Span,
513
+
local_span: Span,
514
+
global_offset: usize,
515
+
) -> Vec<CompletionContext> {
516
+
if idx == 0 {
517
+
return vec![CompletionContext {
518
+
kind: CompletionKind::Argument,
519
+
prefix: prefix.to_string(),
520
+
span,
521
+
}];
522
+
}
523
+
524
+
let prev_shape = &shapes[idx - 1];
525
+
let prev_local_span = to_local_span(prev_shape.0, global_offset);
526
+
527
+
if let FlatShape::Variable(var_id) = prev_shape.1 {
528
+
// Check if the variable shape ends right where this shape starts
529
+
if prev_local_span.end == local_span.start {
530
+
let trimmed_prefix = prefix.trim();
531
+
// Parse path members from the prefix (which is like ".field" or ".field.subfield")
532
+
let after_dot = &trimmed_prefix[1..]; // Remove leading dot
533
+
let (path_so_far, cell_prefix) = if after_dot.is_empty() {
534
+
(vec![], "")
535
+
} else {
536
+
parse_cell_path_from_fields(after_dot)
537
+
};
538
+
539
+
let prefix_byte_len = cell_prefix.len();
540
+
let cell_span_start = span.end.saturating_sub(prefix_byte_len);
541
+
console_log!(
542
+
"[completion] Detected cell path from adjacent Variable shape, var_id={var_id:?}, prefix={cell_prefix:?}"
543
+
);
544
+
vec![CompletionContext {
545
+
kind: CompletionKind::CellPath {
546
+
var_id,
547
+
path_so_far: path_so_far.iter().map(|s| s.to_string()).collect(),
548
+
},
549
+
prefix: cell_prefix.to_string(),
550
+
span: Span::new(cell_span_start, span.end),
551
+
}]
552
+
} else {
553
+
// Gap between shapes, fall through to default handling
554
+
vec![CompletionContext {
555
+
kind: CompletionKind::Argument,
556
+
prefix: prefix.to_string(),
557
+
span,
558
+
}]
559
+
}
560
+
} else {
561
+
// Previous shape is not a Variable, this is likely a file path starting with .
562
+
vec![CompletionContext {
563
+
kind: CompletionKind::Argument,
564
+
prefix: prefix.to_string(),
565
+
span,
566
+
}]
567
+
}
568
+
}
569
+
570
+
pub fn determine_context_from_shape(
571
+
input: &str,
572
+
shapes: &[(Span, FlatShape)],
573
+
working_set: &StateWorkingSet,
574
+
engine_guard: &EngineState,
575
+
byte_pos: usize,
576
+
global_offset: usize,
577
+
) -> Vec<CompletionContext> {
578
+
// First, check if cursor is within a shape
579
+
for (idx, (span, shape)) in shapes.iter().enumerate() {
580
+
let local_span = to_local_span(*span, global_offset);
581
+
582
+
if local_span.start <= byte_pos && byte_pos <= local_span.end {
583
+
console_log!("[completion] Cursor in shape {idx}: {shape:?} at {local_span:?}");
584
+
585
+
// Check if there's a pipe or semicolon between this shape's end and the cursor
586
+
// If so, we're starting a new command and should ignore this shape
587
+
let has_sep = has_separator_between(input, local_span.end, byte_pos);
588
+
if has_sep {
589
+
console_log!(
590
+
"[completion] Separator found between shape end ({end}) and cursor ({byte_pos}), skipping shape",
591
+
end = local_span.end
592
+
);
593
+
// There's a separator, so we're starting a new command - skip this shape
594
+
continue;
595
+
}
596
+
597
+
let span = Span::new(local_span.start, std::cmp::min(local_span.end, byte_pos));
598
+
let prefix = safe_slice(input, span);
599
+
console_log!("[completion] Processing shape {idx} with prefix: {prefix:?}");
600
+
601
+
// Special case: if prefix is just '{' (possibly with whitespace),
602
+
// we're at the start of a block and should complete commands
603
+
let trimmed_prefix = prefix.trim();
604
+
if trimmed_prefix == "{" {
605
+
// We're right after '{' - command context
606
+
if let Some((_, adjusted_span, _)) = handle_block_prefix(&prefix, span) {
607
+
return vec![CompletionContext {
608
+
kind: CompletionKind::Command {
609
+
parent_command: None,
610
+
},
611
+
prefix: String::new(),
612
+
span: adjusted_span,
613
+
}];
614
+
}
615
+
} else {
616
+
match shape {
617
+
// Special case: Check if we're completing a cell path where the Variable and field are in separate shapes
618
+
_ if { idx > 0 && matches!(shape, FlatShape::String) } => {
619
+
let contexts = handle_variable_string_shape(
620
+
input,
621
+
shapes,
622
+
working_set,
623
+
engine_guard,
624
+
idx,
625
+
&prefix,
626
+
span,
627
+
local_span,
628
+
global_offset,
629
+
);
630
+
if !contexts.is_empty() {
631
+
return contexts;
632
+
}
633
+
}
634
+
// Special case: Check if we're completing a cell path where the Variable and dot are in separate shapes
635
+
_ if {
636
+
let trimmed_prefix = prefix.trim();
637
+
trimmed_prefix.starts_with('.') && idx > 0
638
+
} =>
639
+
{
640
+
let contexts = handle_dot_shape(
641
+
input,
642
+
shapes,
643
+
idx,
644
+
&prefix,
645
+
span,
646
+
local_span,
647
+
global_offset,
648
+
);
649
+
if !contexts.is_empty() {
650
+
return contexts;
651
+
}
652
+
}
653
+
_ if {
654
+
// Check if this is a variable or cell path (starts with $) before treating as command
655
+
let trimmed_prefix = prefix.trim();
656
+
trimmed_prefix.starts_with('$')
657
+
} =>
658
+
{
659
+
let trimmed_prefix = prefix.trim();
660
+
// Check if this is a cell path (contains a dot after $)
661
+
if let Some((var_name, path_so_far, cell_prefix)) =
662
+
parse_cell_path(trimmed_prefix)
663
+
{
664
+
// Find the variable ID
665
+
let var_id = lookup_variable_id(var_name, working_set);
666
+
667
+
if let Some(var_id) = var_id {
668
+
// Calculate span for the cell path member being completed
669
+
let prefix_byte_len = cell_prefix.len();
670
+
let cell_span_start = span.end.saturating_sub(prefix_byte_len);
671
+
return vec![CompletionContext {
672
+
kind: CompletionKind::CellPath {
673
+
var_id,
674
+
path_so_far: path_so_far
675
+
.iter()
676
+
.map(|s| s.to_string())
677
+
.collect(),
678
+
},
679
+
prefix: cell_prefix.to_string(),
680
+
span: Span::new(cell_span_start, span.end),
681
+
}];
682
+
} else {
683
+
// Unknown variable, fall back to variable completion
684
+
let var_prefix = trimmed_prefix[1..].to_string();
685
+
return vec![CompletionContext {
686
+
kind: CompletionKind::Variable,
687
+
prefix: var_prefix,
688
+
span,
689
+
}];
690
+
}
691
+
} else {
692
+
// Variable completion context (no dot)
693
+
let var_prefix = if trimmed_prefix.len() > 1 {
694
+
trimmed_prefix[1..].to_string()
695
+
} else {
696
+
String::new()
697
+
};
698
+
return vec![CompletionContext {
699
+
kind: CompletionKind::Variable,
700
+
prefix: var_prefix,
701
+
span,
702
+
}];
703
+
}
704
+
}
705
+
_ if is_command_shape(input, shape, local_span) => {
706
+
let (full_prefix, full_span) =
707
+
build_command_prefix(input, shapes, idx, span, &prefix, global_offset);
708
+
return vec![CompletionContext {
709
+
kind: CompletionKind::Command {
710
+
parent_command: None,
711
+
},
712
+
prefix: full_prefix,
713
+
span: full_span,
714
+
}];
715
+
}
716
+
FlatShape::Block | FlatShape::Closure => {
717
+
let contexts = handle_block_or_closure(
718
+
input,
719
+
shapes,
720
+
working_set,
721
+
engine_guard,
722
+
&prefix,
723
+
span,
724
+
shape.as_str().trim_start_matches("shape_"),
725
+
idx,
726
+
local_span,
727
+
global_offset,
728
+
);
729
+
if !contexts.is_empty() {
730
+
return contexts;
731
+
}
732
+
}
733
+
FlatShape::Variable(var_id) => {
734
+
// Variable or cell path completion context
735
+
let trimmed_prefix = prefix.trim();
736
+
if trimmed_prefix.starts_with('$') {
737
+
// Check if this is a cell path (contains a dot after $)
738
+
if let Some((_, path_so_far, cell_prefix)) =
739
+
parse_cell_path(trimmed_prefix)
740
+
{
741
+
let prefix_byte_len = cell_prefix.len();
742
+
let cell_span_start = span.end.saturating_sub(prefix_byte_len);
743
+
return vec![CompletionContext {
744
+
kind: CompletionKind::CellPath {
745
+
var_id: *var_id,
746
+
path_so_far: path_so_far
747
+
.iter()
748
+
.map(|s| s.to_string())
749
+
.collect(),
750
+
},
751
+
prefix: cell_prefix.to_string(),
752
+
span: Span::new(cell_span_start, span.end),
753
+
}];
754
+
} else {
755
+
// Simple variable completion
756
+
let var_prefix = trimmed_prefix[1..].to_string();
757
+
return vec![CompletionContext {
758
+
kind: CompletionKind::Variable,
759
+
prefix: var_prefix,
760
+
span,
761
+
}];
762
+
}
763
+
} else {
764
+
// Fallback to argument context if no $ found
765
+
return vec![CompletionContext {
766
+
kind: CompletionKind::Argument,
767
+
prefix: prefix.to_string(),
768
+
span,
769
+
}];
770
+
}
771
+
}
772
+
_ => {
773
+
// Check if this is a variable or cell path (starts with $)
774
+
let trimmed_prefix = prefix.trim();
775
+
if trimmed_prefix.starts_with('$') {
776
+
// Check if this is a cell path (contains a dot after $)
777
+
if let Some((var_name, path_so_far, cell_prefix)) =
778
+
parse_cell_path(trimmed_prefix)
779
+
{
780
+
let var_id = lookup_variable_id(var_name, working_set);
781
+
if let Some(var_id) = var_id {
782
+
let prefix_byte_len = cell_prefix.len();
783
+
let cell_span_start = span.end.saturating_sub(prefix_byte_len);
784
+
return vec![CompletionContext {
785
+
kind: CompletionKind::CellPath {
786
+
var_id,
787
+
path_so_far: path_so_far
788
+
.iter()
789
+
.map(|s| s.to_string())
790
+
.collect(),
791
+
},
792
+
prefix: cell_prefix.to_string(),
793
+
span: Span::new(cell_span_start, span.end),
794
+
}];
795
+
} else {
796
+
let var_prefix = trimmed_prefix[1..].to_string();
797
+
return vec![CompletionContext {
798
+
kind: CompletionKind::Variable,
799
+
prefix: var_prefix,
800
+
span,
801
+
}];
802
+
}
803
+
} else {
804
+
// Simple variable completion
805
+
let var_prefix = if trimmed_prefix.len() > 1 {
806
+
trimmed_prefix[1..].to_string()
807
+
} else {
808
+
String::new()
809
+
};
810
+
return vec![CompletionContext {
811
+
kind: CompletionKind::Variable,
812
+
prefix: var_prefix,
813
+
span,
814
+
}];
815
+
}
816
+
} else {
817
+
// Use helper to determine flag or argument context
818
+
return determine_flag_or_argument_context(
819
+
input,
820
+
shapes,
821
+
&trimmed_prefix,
822
+
idx,
823
+
local_span,
824
+
span,
825
+
global_offset,
826
+
working_set,
827
+
engine_guard,
828
+
);
829
+
}
830
+
}
831
+
}
832
+
}
833
+
break;
834
+
}
835
+
}
836
+
Vec::new()
837
+
}
838
+
839
+
pub fn determine_context_fallback(
840
+
input: &str,
841
+
shapes: &[(Span, FlatShape)],
842
+
working_set: &StateWorkingSet,
843
+
engine_guard: &EngineState,
844
+
byte_pos: usize,
845
+
global_offset: usize,
846
+
) -> Vec<CompletionContext> {
847
+
use nu_parser::{TokenContents, lex};
848
+
849
+
console_log!("[completion] Context is None, entering fallback logic");
850
+
// Check if there's a command-like shape before us
851
+
let mut has_separator_after_command = false;
852
+
for (span, shape) in shapes.iter().rev() {
853
+
let local_span = to_local_span(*span, global_offset);
854
+
if local_span.end <= byte_pos {
855
+
if is_command_shape(input, shape, local_span) {
856
+
// Check if there's a pipe or semicolon between this command and the cursor
857
+
has_separator_after_command =
858
+
has_separator_between(input, local_span.end, byte_pos);
859
+
console_log!(
860
+
"[completion] Found command shape {shape:?} at {local_span:?}, has_separator_after_command={has_separator_after_command}"
861
+
);
862
+
if !has_separator_after_command {
863
+
// Extract the command text (full command including subcommands)
864
+
let cmd = safe_slice(input, local_span);
865
+
let cmd_full = cmd.trim().to_string();
866
+
let cmd_first_word = extract_command_name(cmd).to_string();
867
+
868
+
// Check if we're right after the command (only whitespace between command and cursor)
869
+
let text_after_command = if local_span.end < input.len() {
870
+
&input[local_span.end..byte_pos]
871
+
} else {
872
+
""
873
+
};
874
+
let is_right_after_command = text_after_command.trim().is_empty();
875
+
876
+
// If we're right after a command, check if it has positional arguments
877
+
if is_right_after_command {
878
+
// Check if the command text contains spaces (indicating it's a subcommand like "attr category")
879
+
let is_subcommand = cmd_full.contains(' ') && cmd_full != cmd_first_word;
880
+
881
+
// First, try the full command name (e.g., "attr category")
882
+
// If that doesn't exist, fall back to the first word (e.g., "attr")
883
+
let full_cmd_exists =
884
+
get_command_signature(engine_guard, &cmd_full).is_some();
885
+
let cmd_name = if full_cmd_exists {
886
+
cmd_full.clone()
887
+
} else {
888
+
cmd_first_word.clone()
889
+
};
890
+
891
+
let mut context = Vec::with_capacity(2);
892
+
if let Some(signature) = get_command_signature(engine_guard, &cmd_name) {
893
+
// Check if command has any positional arguments
894
+
let has_positional_args = !signature.required_positional.is_empty()
895
+
|| !signature.optional_positional.is_empty();
896
+
897
+
if has_positional_args {
898
+
// Count existing arguments before cursor
899
+
let mut arg_count = 0;
900
+
for (prev_span, prev_shape) in shapes.iter().rev() {
901
+
let prev_local_span = to_local_span(*prev_span, global_offset);
902
+
if prev_local_span.end <= byte_pos
903
+
&& prev_local_span.end > local_span.end
904
+
{
905
+
if !is_command_shape(input, prev_shape, prev_local_span) {
906
+
let arg_text = safe_slice(input, prev_local_span);
907
+
let trimmed_arg = arg_text.trim();
908
+
// Don't count flags (starting with -) or empty arguments
909
+
if !trimmed_arg.is_empty()
910
+
&& !trimmed_arg.starts_with('-')
911
+
{
912
+
arg_count += 1;
913
+
}
914
+
}
915
+
}
916
+
}
917
+
918
+
console_log!(
919
+
"[completion] Right after command {cmd_name:?}, setting CommandArgument context with arg_index: {arg_count}"
920
+
);
921
+
922
+
// Use helper to create CommandArgument context(s) - may include subcommand context
923
+
let arg_contexts = create_command_argument_contexts(
924
+
cmd_name.clone(),
925
+
arg_count,
926
+
String::new(),
927
+
Span::new(byte_pos, byte_pos),
928
+
working_set,
929
+
engine_guard,
930
+
);
931
+
context.extend(arg_contexts);
932
+
}
933
+
}
934
+
// No positional arguments
935
+
// If this is a subcommand (contains spaces), don't show subcommands
936
+
// Only show subcommands if we're using just the base command (single word)
937
+
if is_subcommand && full_cmd_exists {
938
+
console_log!(
939
+
"[completion] Command {cmd_name:?} is a subcommand with no positional args, not showing completions"
940
+
);
941
+
} else {
942
+
// Show subcommands of the base command
943
+
console_log!(
944
+
"[completion] Command {cmd_name:?} has no positional args, showing subcommands"
945
+
);
946
+
context.push(CompletionContext {
947
+
kind: CompletionKind::Command {
948
+
parent_command: Some(cmd_first_word),
949
+
},
950
+
prefix: String::new(),
951
+
span: Span::new(byte_pos, byte_pos),
952
+
});
953
+
}
954
+
// reverse to put subcommands in the beginning
955
+
context.reverse();
956
+
return context;
957
+
} else {
958
+
// Not right after command, complete the command itself
959
+
console_log!("[completion] Set Command context with prefix: {cmd:?}");
960
+
return vec![CompletionContext {
961
+
kind: CompletionKind::Command {
962
+
parent_command: None,
963
+
},
964
+
prefix: cmd.to_string(),
965
+
span: local_span,
966
+
}];
967
+
}
968
+
}
969
+
}
970
+
break;
971
+
}
972
+
}
973
+
974
+
// No command found before, check context from tokens
975
+
console_log!("[completion] No command found before cursor, checking tokens");
976
+
// No command before, check context from tokens
977
+
let (tokens, _) = lex(input.as_bytes(), 0, &[], &[], true);
978
+
let last_token = tokens.iter().filter(|t| t.span.end <= byte_pos).last();
979
+
980
+
let is_cmd_context = if let Some(token) = last_token {
981
+
let matches = matches!(
982
+
token.contents,
983
+
TokenContents::Pipe
984
+
| TokenContents::PipePipe
985
+
| TokenContents::Semicolon
986
+
| TokenContents::Eol
987
+
);
988
+
console_log!(
989
+
"[completion] Last token: {contents:?}, is_cmd_context from token={matches}",
990
+
contents = token.contents
991
+
);
992
+
matches
993
+
} else {
994
+
console_log!(
995
+
"[completion] No last token found, assuming start of input (is_cmd_context=true)"
996
+
);
997
+
true // Start of input
998
+
};
999
+
1000
+
// Look for the last non-whitespace token before cursor
1001
+
let text_before = &input[..byte_pos];
1002
+
1003
+
// Also check if we're inside a block - if the last non-whitespace char before cursor is '{'
1004
+
let text_before_trimmed = text_before.trim_end();
1005
+
let is_inside_block = text_before_trimmed.ends_with('{');
1006
+
// If we found a separator after a command, we're starting a new command
1007
+
let is_cmd_context = is_cmd_context || is_inside_block || has_separator_after_command;
1008
+
console_log!(
1009
+
"[completion] is_inside_block={is_inside_block}, has_separator_after_command={has_separator_after_command}, final is_cmd_context={is_cmd_context}"
1010
+
);
1011
+
1012
+
// Find the last word before cursor
1013
+
let last_word_start = text_before
1014
+
.rfind(|c: char| c.is_whitespace() || is_separator_char(c))
1015
+
.map(|i| i + 1)
1016
+
.unwrap_or(0);
1017
+
1018
+
let last_word = text_before[last_word_start..].trim_start();
1019
+
console_log!("[completion] last_word_start={last_word_start}, last_word={last_word:?}");
1020
+
1021
+
if is_cmd_context {
1022
+
vec![CompletionContext {
1023
+
kind: CompletionKind::Command {
1024
+
parent_command: None,
1025
+
},
1026
+
prefix: last_word.to_string(),
1027
+
span: Span::new(last_word_start, byte_pos),
1028
+
}]
1029
+
} else {
1030
+
// Check if this is a variable or cell path (starts with $)
1031
+
let trimmed_word = last_word.trim();
1032
+
if trimmed_word.starts_with('$') {
1033
+
// Check if this is a cell path (contains a dot after $)
1034
+
if let Some((var_name, path_so_far, cell_prefix)) = parse_cell_path(trimmed_word) {
1035
+
let var_id = lookup_variable_id(&var_name, working_set);
1036
+
1037
+
if let Some(var_id) = var_id {
1038
+
let prefix_byte_len = cell_prefix.len();
1039
+
let cell_span_start = byte_pos.saturating_sub(prefix_byte_len);
1040
+
vec![CompletionContext {
1041
+
kind: CompletionKind::CellPath {
1042
+
var_id,
1043
+
path_so_far: path_so_far.iter().map(|s| s.to_string()).collect(),
1044
+
},
1045
+
prefix: cell_prefix.to_string(),
1046
+
span: Span::new(cell_span_start, byte_pos),
1047
+
}]
1048
+
} else {
1049
+
let var_prefix = trimmed_word[1..].to_string();
1050
+
vec![CompletionContext {
1051
+
kind: CompletionKind::Variable,
1052
+
prefix: var_prefix,
1053
+
span: Span::new(last_word_start, byte_pos),
1054
+
}]
1055
+
}
1056
+
} else {
1057
+
// Simple variable completion
1058
+
let var_prefix = trimmed_word[1..].to_string();
1059
+
vec![CompletionContext {
1060
+
kind: CompletionKind::Variable,
1061
+
prefix: var_prefix,
1062
+
span: Span::new(last_word_start, byte_pos),
1063
+
}]
1064
+
}
1065
+
} else if trimmed_word.starts_with('-') {
1066
+
// Try to find command by looking backwards through shapes
1067
+
let mut found_cmd = None;
1068
+
for (span, shape) in shapes.iter().rev() {
1069
+
let local_span = to_local_span(*span, global_offset);
1070
+
if local_span.end <= byte_pos && is_command_shape(input, shape, local_span) {
1071
+
let cmd_text = safe_slice(input, local_span);
1072
+
let cmd_name = extract_command_name(cmd_text).to_string();
1073
+
found_cmd = Some(cmd_name);
1074
+
break;
1075
+
}
1076
+
}
1077
+
if let Some(cmd_name) = found_cmd {
1078
+
vec![CompletionContext {
1079
+
kind: CompletionKind::Flag {
1080
+
command_name: cmd_name,
1081
+
},
1082
+
prefix: trimmed_word.to_string(),
1083
+
span: Span::new(last_word_start, byte_pos),
1084
+
}]
1085
+
} else {
1086
+
vec![CompletionContext {
1087
+
kind: CompletionKind::Argument,
1088
+
prefix: last_word.to_string(),
1089
+
span: Span::new(last_word_start, byte_pos),
1090
+
}]
1091
+
}
1092
+
} else {
1093
+
// Try to find command and argument index
1094
+
let mut found_cmd = None;
1095
+
let mut arg_count = 0;
1096
+
for (span, shape) in shapes.iter().rev() {
1097
+
let local_span = to_local_span(*span, global_offset);
1098
+
if local_span.end <= byte_pos {
1099
+
if is_command_shape(input, shape, local_span) {
1100
+
let cmd_text = safe_slice(input, local_span);
1101
+
let cmd_name = extract_command_name(cmd_text).to_string();
1102
+
found_cmd = Some(cmd_name);
1103
+
break;
1104
+
} else {
1105
+
let arg_text = safe_slice(input, local_span);
1106
+
let trimmed_arg = arg_text.trim();
1107
+
if !trimmed_arg.is_empty() && !trimmed_arg.starts_with('-') {
1108
+
arg_count += 1;
1109
+
}
1110
+
}
1111
+
}
1112
+
}
1113
+
if let Some(cmd_name) = found_cmd {
1114
+
create_command_argument_contexts(
1115
+
cmd_name,
1116
+
arg_count,
1117
+
trimmed_word.to_string(),
1118
+
Span::new(last_word_start, byte_pos),
1119
+
working_set,
1120
+
engine_guard,
1121
+
)
1122
+
} else {
1123
+
vec![CompletionContext {
1124
+
kind: CompletionKind::Argument,
1125
+
prefix: last_word.to_string(),
1126
+
span: Span::new(last_word_start, byte_pos),
1127
+
}]
1128
+
}
1129
+
}
1130
+
}
1131
+
}
1132
+
1133
+
pub fn determine_context(
1134
+
input: &str,
1135
+
shapes: &[(Span, FlatShape)],
1136
+
working_set: &StateWorkingSet,
1137
+
engine_guard: &EngineState,
1138
+
byte_pos: usize,
1139
+
global_offset: usize,
1140
+
) -> Vec<CompletionContext> {
1141
+
// First try to determine context from shapes
1142
+
let contexts = determine_context_from_shape(
1143
+
input,
1144
+
shapes,
1145
+
working_set,
1146
+
engine_guard,
1147
+
byte_pos,
1148
+
global_offset,
1149
+
);
1150
+
if !contexts.is_empty() {
1151
+
return contexts;
1152
+
}
1153
+
1154
+
// Fallback to token-based context determination
1155
+
determine_context_fallback(
1156
+
input,
1157
+
shapes,
1158
+
working_set,
1159
+
engine_guard,
1160
+
byte_pos,
1161
+
global_offset,
1162
+
)
1163
+
}
+169
src/completion/helpers.rs
+169
src/completion/helpers.rs
···
1
+
use nu_parser::FlatShape;
2
+
use nu_protocol::engine::StateWorkingSet;
3
+
use nu_protocol::{ENV_VARIABLE_ID, IN_VARIABLE_ID, NU_VARIABLE_ID, Span};
4
+
5
+
/// Macro for console logging that automatically converts formatted strings to JsValue
6
+
#[macro_export]
7
+
macro_rules! console_log {
8
+
($($arg:tt)*) => {
9
+
#[cfg(debug_assertions)]
10
+
web_sys::console::log_1(&wasm_bindgen::JsValue::from_str(&format!($($arg)*)));
11
+
};
12
+
}
13
+
14
+
pub fn is_separator_char(c: char) -> bool {
15
+
['|', ';', '(', '{'].contains(&c)
16
+
}
17
+
18
+
pub fn is_command_separator_char(c: char) -> bool {
19
+
['|', ';'].contains(&c)
20
+
}
21
+
22
+
pub fn has_separator_between(input: &str, start: usize, end: usize) -> bool {
23
+
if start < end && start < input.len() {
24
+
let text_between = &input[start..std::cmp::min(end, input.len())];
25
+
text_between.chars().any(|c| is_separator_char(c))
26
+
} else {
27
+
false
28
+
}
29
+
}
30
+
31
+
pub fn find_last_separator_pos(text: &str) -> Option<usize> {
32
+
text.rfind(|c| is_command_separator_char(c)).map(|i| i + 1)
33
+
}
34
+
35
+
pub fn ends_with_separator(text: &str) -> bool {
36
+
let text = text.trim_end();
37
+
text.ends_with('|') || text.ends_with(';')
38
+
}
39
+
40
+
pub fn to_local_span(span: Span, global_offset: usize) -> Span {
41
+
Span::new(
42
+
span.start.saturating_sub(global_offset),
43
+
span.end.saturating_sub(global_offset),
44
+
)
45
+
}
46
+
47
+
pub fn safe_slice(input: &str, span: Span) -> &str {
48
+
if span.start < input.len() {
49
+
let safe_end = std::cmp::min(span.end, input.len());
50
+
&input[span.start..safe_end]
51
+
} else {
52
+
""
53
+
}
54
+
}
55
+
56
+
pub fn is_command_shape(input: &str, shape: &FlatShape, local_span: Span) -> bool {
57
+
matches!(
58
+
shape,
59
+
FlatShape::External(_) | FlatShape::InternalCall(_) | FlatShape::Keyword
60
+
) || matches!(shape, FlatShape::Garbage) && {
61
+
if local_span.start < input.len() {
62
+
let prev_text = safe_slice(input, local_span);
63
+
!prev_text.trim().starts_with('-')
64
+
} else {
65
+
false
66
+
}
67
+
}
68
+
}
69
+
70
+
pub fn handle_block_prefix(prefix: &str, span: Span) -> Option<(&str, Span, bool)> {
71
+
let mut block_prefix = prefix;
72
+
let mut block_span_start = span.start;
73
+
74
+
// Remove leading '{' and whitespace
75
+
if block_prefix.starts_with('{') {
76
+
block_prefix = &block_prefix[1..];
77
+
block_span_start += 1;
78
+
}
79
+
let trimmed_block_prefix = block_prefix.trim_start();
80
+
if trimmed_block_prefix != block_prefix {
81
+
// Adjust span start to skip whitespace
82
+
block_span_start += block_prefix.len() - trimmed_block_prefix.len();
83
+
}
84
+
85
+
let is_empty = trimmed_block_prefix.is_empty();
86
+
Some((
87
+
trimmed_block_prefix,
88
+
Span::new(block_span_start, span.end),
89
+
is_empty,
90
+
))
91
+
}
92
+
93
+
pub fn extract_command_name(cmd_text: &str) -> &str {
94
+
cmd_text
95
+
.split_whitespace()
96
+
.next()
97
+
.unwrap_or(cmd_text)
98
+
.trim()
99
+
}
100
+
101
+
pub fn lookup_variable_id(
102
+
var_name: &str,
103
+
working_set: &StateWorkingSet,
104
+
) -> Option<nu_protocol::VarId> {
105
+
match var_name {
106
+
"env" => Some(ENV_VARIABLE_ID),
107
+
"nu" => Some(NU_VARIABLE_ID),
108
+
"in" => Some(IN_VARIABLE_ID),
109
+
_ => working_set.find_variable(var_name.as_bytes()),
110
+
}
111
+
}
112
+
113
+
pub fn parse_cell_path(text: &str) -> Option<(&str, Vec<&str>, &str)> {
114
+
let trimmed = text.trim();
115
+
if !trimmed.starts_with('$') {
116
+
return None;
117
+
}
118
+
119
+
// Check if this is a cell path (contains a dot after $)
120
+
if let Some(dot_pos) = trimmed[1..].find('.') {
121
+
let var_name = &trimmed[1..dot_pos + 1];
122
+
let after_var = &trimmed[dot_pos + 2..];
123
+
let parts: Vec<&str> = after_var.split('.').collect();
124
+
let (path_so_far, cell_prefix) = if parts.is_empty() {
125
+
(vec![], "")
126
+
} else if after_var.ends_with('.') {
127
+
(
128
+
parts.iter().filter(|s| !s.is_empty()).copied().collect(),
129
+
"",
130
+
)
131
+
} else {
132
+
let path: Vec<&str> = parts[..parts.len().saturating_sub(1)]
133
+
.iter()
134
+
.copied()
135
+
.collect();
136
+
let prefix = parts.last().copied().unwrap_or("");
137
+
(path, prefix)
138
+
};
139
+
Some((var_name, path_so_far, cell_prefix))
140
+
} else {
141
+
None
142
+
}
143
+
}
144
+
145
+
pub fn parse_cell_path_from_fields(text: &str) -> (Vec<&str>, &str) {
146
+
let trimmed = text.trim();
147
+
let parts: Vec<&str> = trimmed.split('.').collect();
148
+
if parts.is_empty() {
149
+
(vec![], "")
150
+
} else if trimmed.ends_with('.') {
151
+
(
152
+
parts.iter().filter(|s| !s.is_empty()).copied().collect(),
153
+
"",
154
+
)
155
+
} else {
156
+
let path: Vec<&str> = parts[..parts.len().saturating_sub(1)]
157
+
.iter()
158
+
.copied()
159
+
.collect();
160
+
let prefix = parts.last().copied().unwrap_or("");
161
+
(path, prefix)
162
+
}
163
+
}
164
+
165
+
pub fn to_char_span(input: &str, span: Span) -> Span {
166
+
let char_start = input[..span.start].chars().count();
167
+
let char_end = input[..span.end].chars().count();
168
+
Span::new(char_start, char_end)
169
+
}
+97
src/completion/mod.rs
+97
src/completion/mod.rs
···
1
+
use crate::console_log;
2
+
use futures::FutureExt;
3
+
use js_sys::Promise;
4
+
use nu_parser::{flatten_block, parse};
5
+
use nu_protocol::engine::StateWorkingSet;
6
+
use wasm_bindgen::prelude::*;
7
+
use wasm_bindgen_futures::future_to_promise;
8
+
9
+
use super::*;
10
+
11
+
pub mod context;
12
+
pub mod helpers;
13
+
pub mod suggestions;
14
+
pub mod types;
15
+
pub mod variables;
16
+
17
+
pub use context::determine_context;
18
+
pub use suggestions::generate_suggestions;
19
+
pub use types::{CompletionContext, Suggestion};
20
+
21
+
#[wasm_bindgen]
22
+
pub fn completion(input: String, js_cursor_pos: usize) -> Promise {
23
+
future_to_promise(completion_impl(input, js_cursor_pos).map(|s| Ok(JsValue::from_str(&s))))
24
+
}
25
+
26
+
pub async fn completion_impl(input: String, js_cursor_pos: usize) -> String {
27
+
let engine_guard = read_engine_state().await;
28
+
let stack_guard = crate::read_stack().await;
29
+
let root = get_pwd();
30
+
31
+
// Map UTF-16 cursor position (from JS) to Byte index (for Rust)
32
+
let byte_pos = input
33
+
.char_indices()
34
+
.map(|(i, _)| i)
35
+
.nth(js_cursor_pos)
36
+
.unwrap_or(input.len());
37
+
38
+
let (working_set, shapes, global_offset) = {
39
+
let mut working_set = StateWorkingSet::new(&engine_guard);
40
+
let global_offset = working_set.next_span_start();
41
+
let block = parse(&mut working_set, None, input.as_bytes(), false);
42
+
let shapes = flatten_block(&working_set, &block);
43
+
(working_set, shapes, global_offset)
44
+
};
45
+
46
+
// Initial state logging
47
+
console_log!(
48
+
"[completion] Input: {input:?}, JS cursor: {js_cursor_pos}, byte cursor: {byte_pos}"
49
+
);
50
+
console_log!(
51
+
"[completion] Found {count} shapes, global_offset: {global_offset}",
52
+
count = shapes.len()
53
+
);
54
+
for (idx, (span, shape)) in shapes.iter().enumerate() {
55
+
let (local_start, local_end) = (
56
+
span.start.saturating_sub(global_offset),
57
+
span.end.saturating_sub(global_offset),
58
+
);
59
+
console_log!(
60
+
"[completion] Shape {idx}: {shape:?} at [{start}, {end}] (local: [{local_start}, {local_end}])",
61
+
start = span.start,
62
+
end = span.end
63
+
);
64
+
}
65
+
66
+
// Determine completion context
67
+
let context = determine_context(
68
+
&input,
69
+
&shapes,
70
+
&working_set,
71
+
&engine_guard,
72
+
byte_pos,
73
+
global_offset,
74
+
);
75
+
76
+
// Convert Vec to HashSet
77
+
use std::collections::HashSet;
78
+
let context_set: HashSet<CompletionContext> = context.into_iter().collect();
79
+
80
+
// Generate suggestions based on context
81
+
let suggestions = generate_suggestions(
82
+
&input,
83
+
context_set,
84
+
&working_set,
85
+
&engine_guard,
86
+
&stack_guard,
87
+
&root,
88
+
byte_pos,
89
+
);
90
+
91
+
drop(working_set);
92
+
drop(engine_guard);
93
+
94
+
let suggestions = serde_json::to_string(&suggestions).unwrap_or_else(|_| "[]".to_string());
95
+
console_log!("{suggestions}");
96
+
suggestions
97
+
}
+602
src/completion/suggestions.rs
+602
src/completion/suggestions.rs
···
1
+
use crate::completion::context::get_command_signature;
2
+
use crate::completion::helpers::to_char_span;
3
+
use crate::completion::types::{CompletionContext, CompletionKind, Suggestion};
4
+
use crate::completion::variables::*;
5
+
use crate::console_log;
6
+
use nu_protocol::Span;
7
+
use nu_protocol::engine::{EngineState, Stack, StateWorkingSet};
8
+
use std::collections::HashSet;
9
+
10
+
pub fn generate_command_suggestions(
11
+
input: &str,
12
+
working_set: &StateWorkingSet,
13
+
prefix: String,
14
+
span: Span,
15
+
parent_command: Option<String>,
16
+
) -> Vec<Suggestion> {
17
+
console_log!(
18
+
"[completion] Generating Command suggestions with prefix: {prefix:?}, parent_command: {parent_command:?}"
19
+
);
20
+
21
+
let span = to_char_span(input, span);
22
+
let mut suggestions = Vec::new();
23
+
let mut cmd_count = 0;
24
+
25
+
// Determine search prefix and name extraction logic
26
+
let (search_prefix, parent_prefix_opt) = if let Some(parent) = &parent_command {
27
+
// Show only subcommands of the parent command
28
+
// Subcommands are commands that start with "parent_command " (with space)
29
+
let parent_prefix = format!("{} ", parent);
30
+
let search_prefix = if prefix.is_empty() {
31
+
parent_prefix.clone()
32
+
} else {
33
+
format!("{}{}", parent_prefix, prefix)
34
+
};
35
+
(search_prefix, Some(parent_prefix))
36
+
} else {
37
+
// Regular command completion - show all commands
38
+
(prefix.clone(), None)
39
+
};
40
+
41
+
let cmds = working_set
42
+
.find_commands_by_predicate(|value| value.starts_with(search_prefix.as_bytes()), true);
43
+
44
+
for (_, name, desc, _) in cmds {
45
+
let name_str = String::from_utf8_lossy(&name).to_string();
46
+
47
+
// Extract the command name to display
48
+
// For subcommands, extract just the subcommand name (part after "parent_command ")
49
+
// For regular commands, use the full command name
50
+
let display_name = if let Some(parent_prefix) = &parent_prefix_opt {
51
+
if let Some(subcommand_name) = name_str.strip_prefix(parent_prefix) {
52
+
subcommand_name.to_string()
53
+
} else {
54
+
continue; // Skip if it doesn't match the parent prefix
55
+
}
56
+
} else {
57
+
name_str
58
+
};
59
+
60
+
suggestions.push(Suggestion {
61
+
rendered: {
62
+
let name_colored = ansi_term::Color::Green.bold().paint(&display_name);
63
+
let desc_str = desc.as_deref().unwrap_or("<no description>");
64
+
format!("{name_colored} {desc_str}")
65
+
},
66
+
name: display_name,
67
+
description: desc.map(|d| d.to_string()),
68
+
span_start: span.start,
69
+
span_end: span.end,
70
+
});
71
+
cmd_count += 1;
72
+
}
73
+
console_log!("[completion] Found {cmd_count} command suggestions");
74
+
suggestions.sort();
75
+
suggestions
76
+
}
77
+
78
+
pub fn generate_argument_suggestions(
79
+
input: &str,
80
+
prefix: String,
81
+
span: Span,
82
+
root: &std::sync::Arc<vfs::VfsPath>,
83
+
) -> Vec<Suggestion> {
84
+
console_log!("[completion] Generating Argument suggestions with prefix: {prefix:?}");
85
+
// File completion
86
+
let mut file_suggestions = generate_file_suggestions(&prefix, span, root, None, input);
87
+
console_log!(
88
+
"[completion] Found {file_count} file suggestions",
89
+
file_count = file_suggestions.len()
90
+
);
91
+
file_suggestions.sort();
92
+
file_suggestions
93
+
}
94
+
95
+
pub fn generate_flag_suggestions(
96
+
input: &str,
97
+
engine_guard: &EngineState,
98
+
prefix: String,
99
+
span: Span,
100
+
command_name: String,
101
+
) -> Vec<Suggestion> {
102
+
console_log!(
103
+
"[completion] Generating Flag suggestions for command: {command_name:?}, prefix: {prefix:?}"
104
+
);
105
+
106
+
let mut suggestions = Vec::new();
107
+
if let Some(signature) = get_command_signature(engine_guard, &command_name) {
108
+
let span = to_char_span(input, span);
109
+
let mut flag_count = 0;
110
+
111
+
// Get switches from signature
112
+
// Signature has a named field that contains named arguments (including switches)
113
+
for flag in &signature.named {
114
+
// Check if this is a switch (has no argument)
115
+
// Switches have arg: None, named arguments have arg: Some(SyntaxShape)
116
+
let is_switch = flag.arg.is_none();
117
+
118
+
if is_switch {
119
+
let long_name = format!("--{}", flag.long);
120
+
let short_name = flag.short.map(|c| format!("-{}", c));
121
+
122
+
// Determine which flags to show based on prefix:
123
+
// - If prefix is empty or exactly "-", show all flags (both short and long)
124
+
// - If prefix starts with "--", only show long flags that match the prefix
125
+
// - If prefix starts with "-" (but not "--"), only show short flags that match the prefix
126
+
let show_all = prefix.is_empty() || prefix == "-";
127
+
128
+
// Helper to create a flag suggestion
129
+
let create_flag_suggestion = |flag_name: String| -> Suggestion {
130
+
Suggestion {
131
+
name: flag_name.clone(),
132
+
description: Some(flag.desc.clone()),
133
+
rendered: {
134
+
let flag_colored = ansi_term::Color::Cyan.bold().paint(&flag_name);
135
+
format!("{flag_colored} {}", flag.desc)
136
+
},
137
+
span_start: span.start,
138
+
span_end: span.end,
139
+
}
140
+
};
141
+
142
+
// Add long flag if it matches
143
+
let should_show_long = if show_all {
144
+
true // Show all flags when prefix is "-" or empty
145
+
} else if prefix.starts_with("--") {
146
+
long_name.starts_with(&prefix) // Only show long flags matching prefix
147
+
} else {
148
+
false // Don't show long flags if prefix is short flag format
149
+
};
150
+
151
+
if should_show_long {
152
+
suggestions.push(create_flag_suggestion(long_name));
153
+
flag_count += 1;
154
+
}
155
+
156
+
// Add short flag if it matches
157
+
if let Some(short) = &short_name {
158
+
let flag_char = flag.short.unwrap_or(' ');
159
+
let should_show_short = if show_all {
160
+
true // Show all flags when prefix is "-" or empty
161
+
} else if prefix.starts_with("-") && !prefix.starts_with("--") {
162
+
// For combined short flags like "-a" or "-af", suggest flags that can be appended
163
+
// Extract already used flags from prefix (e.g., "-a" -> ['a'], "-af" -> ['a', 'f'])
164
+
let used_flags: Vec<char> = prefix[1..].chars().collect();
165
+
166
+
// Show if this flag isn't already in the prefix
167
+
!used_flags.contains(&flag_char)
168
+
} else {
169
+
false // Don't show short flags if prefix is long flag format
170
+
};
171
+
172
+
if should_show_short {
173
+
// If prefix already contains flags (like "-a"), create combined suggestion (like "-af")
174
+
let suggestion_name = if prefix.len() > 1 && prefix.starts_with("-") {
175
+
format!("{}{}", prefix, flag_char)
176
+
} else {
177
+
short.clone()
178
+
};
179
+
suggestions.push(create_flag_suggestion(suggestion_name));
180
+
flag_count += 1;
181
+
}
182
+
}
183
+
}
184
+
}
185
+
186
+
console_log!("[completion] Found {flag_count} flag suggestions");
187
+
} else {
188
+
console_log!("[completion] Could not find signature for command: {command_name:?}");
189
+
}
190
+
suggestions.sort();
191
+
suggestions
192
+
}
193
+
194
+
pub fn generate_command_argument_suggestions(
195
+
input: &str,
196
+
engine_guard: &EngineState,
197
+
_working_set: &StateWorkingSet,
198
+
prefix: String,
199
+
span: Span,
200
+
command_name: String,
201
+
arg_index: usize,
202
+
root: &std::sync::Arc<vfs::VfsPath>,
203
+
) -> Vec<Suggestion> {
204
+
console_log!(
205
+
"[completion] Generating CommandArgument suggestions for command: {command_name:?}, arg_index: {arg_index}, prefix: {prefix:?}"
206
+
);
207
+
208
+
let mut suggestions = Vec::new();
209
+
210
+
if let Some(signature) = get_command_signature(engine_guard, &command_name) {
211
+
// First, check if we're completing an argument for a flag
212
+
// Look backwards from the current position to find the previous flag
213
+
let text_before = if span.start < input.len() {
214
+
&input[..span.start]
215
+
} else {
216
+
""
217
+
};
218
+
let text_before_trimmed = text_before.trim_end();
219
+
220
+
// Check if the last word before cursor is a flag
221
+
let last_word_start = text_before_trimmed
222
+
.rfind(|c: char| c.is_whitespace())
223
+
.map(|i| i + 1)
224
+
.unwrap_or(0);
225
+
let last_word = &text_before_trimmed[last_word_start..];
226
+
227
+
if last_word.starts_with('-') {
228
+
// We're after a flag - check if this flag accepts an argument
229
+
let flag_name = last_word.trim();
230
+
let is_long_flag = flag_name.starts_with("--");
231
+
let flag_to_match: Option<(bool, String)> = if is_long_flag {
232
+
// Long flag: --flag-name
233
+
flag_name.strip_prefix("--").map(|s| (true, s.to_string()))
234
+
} else {
235
+
// Short flag: -f (single character)
236
+
flag_name
237
+
.strip_prefix("-")
238
+
.and_then(|s| s.chars().next().map(|c| (false, c.to_string())))
239
+
};
240
+
241
+
if let Some((is_long, flag_name_to_match)) = flag_to_match {
242
+
// Find the flag in the signature
243
+
for flag in &signature.named {
244
+
let matches_flag = if is_long {
245
+
// Long flag
246
+
flag.long == flag_name_to_match
247
+
} else {
248
+
// Short flag - compare character
249
+
flag.short
250
+
.map(|c| c.to_string() == flag_name_to_match)
251
+
.unwrap_or(false)
252
+
};
253
+
254
+
if matches_flag {
255
+
// Found the flag - check if it accepts an argument
256
+
if let Some(flag_arg_shape) = &flag.arg {
257
+
// Flag accepts an argument - use its type
258
+
console_log!(
259
+
"[completion] Flag {flag_name:?} accepts argument of type {:?}",
260
+
flag_arg_shape
261
+
);
262
+
let mut add_file_suggestions = || {
263
+
let file_suggestions = generate_file_suggestions(
264
+
&prefix,
265
+
span,
266
+
root,
267
+
Some(flag.desc.clone()),
268
+
input,
269
+
);
270
+
let file_count = file_suggestions.len();
271
+
suggestions.extend(file_suggestions);
272
+
console_log!(
273
+
"[completion] Found {file_count} file suggestions for flag argument"
274
+
);
275
+
};
276
+
match flag_arg_shape {
277
+
nu_protocol::SyntaxShape::Filepath
278
+
| nu_protocol::SyntaxShape::Any => {
279
+
add_file_suggestions();
280
+
}
281
+
nu_protocol::SyntaxShape::OneOf(l)
282
+
if l.contains(&nu_protocol::SyntaxShape::Filepath) =>
283
+
{
284
+
add_file_suggestions();
285
+
}
286
+
_ => {
287
+
// Flag argument is not a filepath type
288
+
console_log!(
289
+
"[completion] Flag {flag_name:?} argument is type {:?}, not suggesting files",
290
+
flag_arg_shape
291
+
);
292
+
}
293
+
}
294
+
return suggestions;
295
+
} else {
296
+
// Flag doesn't accept an argument - fall through to positional argument check
297
+
console_log!(
298
+
"[completion] Flag {flag_name:?} doesn't accept an argument, checking positional arguments"
299
+
);
300
+
break;
301
+
}
302
+
}
303
+
}
304
+
}
305
+
}
306
+
307
+
// Not after a flag, or flag doesn't accept an argument - check positional arguments
308
+
// Get positional arguments from signature
309
+
// Check if argument is in required or optional positional
310
+
let required_count = signature.required_positional.len();
311
+
312
+
// Find the argument at the given index
313
+
let arg = if arg_index < signature.required_positional.len() {
314
+
signature.required_positional.get(arg_index)
315
+
} else {
316
+
let optional_index = arg_index - required_count;
317
+
signature.optional_positional.get(optional_index)
318
+
};
319
+
320
+
if let Some(arg) = arg {
321
+
let mut add_file_suggestions = || {
322
+
let file_suggestions =
323
+
generate_file_suggestions(&prefix, span, root, Some(arg.desc.clone()), input);
324
+
let file_count = file_suggestions.len();
325
+
suggestions.extend(file_suggestions);
326
+
console_log!(
327
+
"[completion] Found {file_count} file suggestions for argument {arg_index}"
328
+
);
329
+
};
330
+
331
+
match &arg.shape {
332
+
nu_protocol::SyntaxShape::Filepath | nu_protocol::SyntaxShape::Any => {
333
+
add_file_suggestions();
334
+
}
335
+
nu_protocol::SyntaxShape::OneOf(l)
336
+
if l.contains(&nu_protocol::SyntaxShape::Filepath) =>
337
+
{
338
+
add_file_suggestions();
339
+
}
340
+
_ => {
341
+
// For other types, don't suggest files
342
+
console_log!(
343
+
"[completion] Argument {arg_index} is type {:?}, not suggesting files",
344
+
arg.shape
345
+
);
346
+
}
347
+
}
348
+
} else {
349
+
// Argument index out of range - command doesn't accept that many positional arguments
350
+
// Don't suggest files since we know the type (it's not a valid argument)
351
+
console_log!(
352
+
"[completion] Argument index {arg_index} out of range, not suggesting files"
353
+
);
354
+
}
355
+
} else {
356
+
// No signature found, fall back to file completion
357
+
console_log!(
358
+
"[completion] Could not find signature for command: {command_name:?}, using file completion"
359
+
);
360
+
let file_suggestions = generate_file_suggestions(&prefix, span, root, None, input);
361
+
suggestions.extend(file_suggestions);
362
+
}
363
+
suggestions.sort();
364
+
suggestions
365
+
}
366
+
367
+
pub fn generate_variable_suggestions(
368
+
input: &str,
369
+
working_set: &StateWorkingSet,
370
+
prefix: String,
371
+
span: Span,
372
+
byte_pos: usize,
373
+
) -> Vec<Suggestion> {
374
+
console_log!("[completion] Generating Variable suggestions with prefix: {prefix:?}");
375
+
376
+
// Collect all available variables
377
+
let variables = collect_variables(working_set, input, byte_pos);
378
+
let span = to_char_span(input, span);
379
+
let mut suggestions = Vec::new();
380
+
let mut var_count = 0;
381
+
382
+
for (var_name, var_id) in variables {
383
+
// Filter by prefix (variable name includes $, so we need to check after $)
384
+
if var_name.len() > 1 && var_name[1..].starts_with(&prefix) {
385
+
// Get variable type
386
+
let var_type = working_set.get_variable(var_id).ty.to_string();
387
+
388
+
suggestions.push(Suggestion {
389
+
name: var_name.clone(),
390
+
description: Some(var_type.clone()),
391
+
rendered: {
392
+
let var_colored = ansi_term::Color::Blue.bold().paint(&var_name);
393
+
format!("{var_colored} {var_type}")
394
+
},
395
+
span_start: span.start,
396
+
span_end: span.end,
397
+
});
398
+
var_count += 1;
399
+
}
400
+
}
401
+
402
+
console_log!("[completion] Found {var_count} variable suggestions");
403
+
suggestions.sort();
404
+
suggestions
405
+
}
406
+
407
+
pub fn generate_cell_path_suggestions(
408
+
input: &str,
409
+
working_set: &StateWorkingSet,
410
+
engine_guard: &EngineState,
411
+
stack_guard: &Stack,
412
+
prefix: String,
413
+
span: Span,
414
+
var_id: nu_protocol::VarId,
415
+
path_so_far: Vec<String>,
416
+
) -> Vec<Suggestion> {
417
+
console_log!(
418
+
"[completion] Generating CellPath suggestions with prefix: {prefix:?}, path: {path_so_far:?}"
419
+
);
420
+
421
+
let mut suggestions = Vec::new();
422
+
// Evaluate the variable to get its value
423
+
if let Some(var_value) =
424
+
eval_variable_for_completion(var_id, working_set, engine_guard, stack_guard)
425
+
{
426
+
// Follow the path to get the value at the current level
427
+
let current_value = if path_so_far.is_empty() {
428
+
var_value
429
+
} else {
430
+
let path_refs: Vec<&str> = path_so_far.iter().map(|s| s.as_str()).collect();
431
+
follow_cell_path(&var_value, &path_refs).unwrap_or(var_value)
432
+
};
433
+
434
+
// Get columns/fields from the current value
435
+
let columns = get_columns_from_value(¤t_value);
436
+
let span = to_char_span(input, span);
437
+
let mut field_count = 0;
438
+
439
+
for (col_name, col_type) in columns {
440
+
// Filter by prefix
441
+
if col_name.starts_with(&prefix) {
442
+
let type_str = col_type.as_deref().unwrap_or("any");
443
+
suggestions.push(Suggestion {
444
+
name: col_name.clone(),
445
+
description: Some(type_str.to_string()),
446
+
rendered: {
447
+
let col_colored = ansi_term::Color::Yellow.paint(&col_name);
448
+
format!("{col_colored} {type_str}")
449
+
},
450
+
span_start: span.start,
451
+
span_end: span.end,
452
+
});
453
+
field_count += 1;
454
+
}
455
+
}
456
+
457
+
console_log!("[completion] Found {field_count} cell path suggestions");
458
+
} else {
459
+
// Variable couldn't be evaluated - this is expected for runtime variables
460
+
// We can't provide cell path completions without knowing the structure
461
+
console_log!(
462
+
"[completion] Could not evaluate variable {var_id:?} for cell path completion (runtime variable)"
463
+
);
464
+
465
+
// Try to get type information to provide better feedback
466
+
if let Ok(var_info) = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
467
+
working_set.get_variable(var_id)
468
+
})) {
469
+
console_log!("[completion] Variable type: {ty:?}", ty = var_info.ty);
470
+
}
471
+
}
472
+
suggestions.sort();
473
+
suggestions
474
+
}
475
+
476
+
pub fn generate_file_suggestions(
477
+
prefix: &str,
478
+
span: Span,
479
+
root: &std::sync::Arc<vfs::VfsPath>,
480
+
description: Option<String>,
481
+
input: &str,
482
+
) -> Vec<Suggestion> {
483
+
let (dir, file_prefix) = prefix
484
+
.rfind('/')
485
+
.map(|idx| (&prefix[..idx + 1], &prefix[idx + 1..]))
486
+
.unwrap_or(("", prefix));
487
+
488
+
let dir_to_join = (dir.len() > 1 && dir.ends_with('/'))
489
+
.then(|| &dir[..dir.len() - 1])
490
+
.unwrap_or(dir);
491
+
492
+
let target_dir = if !dir.is_empty() {
493
+
match root.join(dir_to_join) {
494
+
Ok(d) if d.is_dir().unwrap_or(false) => Some(d),
495
+
_ => None,
496
+
}
497
+
} else {
498
+
Some(root.join("").unwrap())
499
+
};
500
+
501
+
let mut file_suggestions = Vec::new();
502
+
if let Some(d) = target_dir {
503
+
if let Ok(iterator) = d.read_dir() {
504
+
let char_span = to_char_span(input, span);
505
+
for entry in iterator {
506
+
let name = entry.filename();
507
+
if name.starts_with(file_prefix) {
508
+
let full_completion = format!("{}{}", dir, name);
509
+
file_suggestions.push(Suggestion {
510
+
name: full_completion.clone(),
511
+
description: description.clone(),
512
+
rendered: full_completion,
513
+
span_start: char_span.start,
514
+
span_end: char_span.end,
515
+
});
516
+
}
517
+
}
518
+
}
519
+
}
520
+
file_suggestions
521
+
}
522
+
523
+
pub fn generate_suggestions(
524
+
input: &str,
525
+
contexts: HashSet<CompletionContext>,
526
+
working_set: &StateWorkingSet,
527
+
engine_guard: &EngineState,
528
+
stack_guard: &Stack,
529
+
root: &std::sync::Arc<vfs::VfsPath>,
530
+
byte_pos: usize,
531
+
) -> Vec<Suggestion> {
532
+
console_log!("contexts: {contexts:?}");
533
+
534
+
let mut context_vec: Vec<_> = contexts.into_iter().collect();
535
+
context_vec.sort_by_key(|ctx| match &ctx.kind {
536
+
CompletionKind::Command { .. } => 0,
537
+
CompletionKind::Flag { .. } => 1,
538
+
CompletionKind::Variable => 2,
539
+
CompletionKind::CellPath { .. } => 3,
540
+
CompletionKind::CommandArgument { .. } => 4,
541
+
CompletionKind::Argument => 5,
542
+
});
543
+
544
+
let mut suggestions = Vec::new();
545
+
for context in context_vec.iter() {
546
+
let mut sug = match &context.kind {
547
+
CompletionKind::Command { parent_command } => generate_command_suggestions(
548
+
input,
549
+
working_set,
550
+
context.prefix.clone(),
551
+
context.span,
552
+
parent_command.clone(),
553
+
),
554
+
CompletionKind::Argument => {
555
+
generate_argument_suggestions(input, context.prefix.clone(), context.span, root)
556
+
}
557
+
CompletionKind::Flag { command_name } => generate_flag_suggestions(
558
+
input,
559
+
engine_guard,
560
+
context.prefix.clone(),
561
+
context.span,
562
+
command_name.clone(),
563
+
),
564
+
CompletionKind::CommandArgument {
565
+
command_name,
566
+
arg_index,
567
+
} => generate_command_argument_suggestions(
568
+
input,
569
+
engine_guard,
570
+
working_set,
571
+
context.prefix.clone(),
572
+
context.span,
573
+
command_name.clone(),
574
+
*arg_index,
575
+
root,
576
+
),
577
+
CompletionKind::Variable => generate_variable_suggestions(
578
+
input,
579
+
working_set,
580
+
context.prefix.clone(),
581
+
context.span,
582
+
byte_pos,
583
+
),
584
+
CompletionKind::CellPath {
585
+
var_id,
586
+
path_so_far,
587
+
} => generate_cell_path_suggestions(
588
+
input,
589
+
working_set,
590
+
engine_guard,
591
+
stack_guard,
592
+
context.prefix.clone(),
593
+
context.span,
594
+
*var_id,
595
+
path_so_far.clone(),
596
+
),
597
+
};
598
+
suggestions.append(&mut sug);
599
+
}
600
+
601
+
suggestions
602
+
}
+88
src/completion/types.rs
+88
src/completion/types.rs
···
1
+
use nu_protocol::Span;
2
+
use serde::Serialize;
3
+
4
+
#[derive(Debug, Serialize)]
5
+
pub struct Suggestion {
6
+
pub name: String,
7
+
pub description: Option<String>,
8
+
pub rendered: String,
9
+
pub span_start: usize, // char index (not byte)
10
+
pub span_end: usize, // char index (not byte)
11
+
}
12
+
13
+
impl PartialEq for Suggestion {
14
+
fn eq(&self, other: &Self) -> bool {
15
+
self.name == other.name
16
+
}
17
+
}
18
+
impl Eq for Suggestion {}
19
+
impl PartialOrd for Suggestion {
20
+
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
21
+
self.name.partial_cmp(&other.name)
22
+
}
23
+
}
24
+
impl Ord for Suggestion {
25
+
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
26
+
self.name.cmp(&other.name)
27
+
}
28
+
}
29
+
30
+
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
31
+
pub enum CompletionKind {
32
+
Command {
33
+
parent_command: Option<String>, // If Some, only show subcommands of this command
34
+
},
35
+
Argument,
36
+
Flag {
37
+
command_name: String,
38
+
},
39
+
CommandArgument {
40
+
command_name: String,
41
+
arg_index: usize,
42
+
},
43
+
Variable, // prefix is without the $ prefix
44
+
CellPath {
45
+
var_id: nu_protocol::VarId, // variable ID for evaluation
46
+
path_so_far: Vec<String>, // path members accessed before current one
47
+
},
48
+
}
49
+
50
+
#[derive(Debug)]
51
+
pub struct CompletionContext {
52
+
pub kind: CompletionKind,
53
+
pub prefix: String, // the partial text being completed
54
+
pub span: Span,
55
+
}
56
+
57
+
impl PartialEq for CompletionContext {
58
+
fn eq(&self, other: &Self) -> bool {
59
+
self.kind == other.kind && self.prefix == other.prefix
60
+
}
61
+
}
62
+
63
+
impl Eq for CompletionContext {}
64
+
65
+
impl PartialOrd for CompletionContext {
66
+
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
67
+
match self.kind.partial_cmp(&other.kind) {
68
+
Some(std::cmp::Ordering::Equal) => self.prefix.partial_cmp(&other.prefix),
69
+
other => other,
70
+
}
71
+
}
72
+
}
73
+
74
+
impl Ord for CompletionContext {
75
+
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
76
+
match self.kind.cmp(&other.kind) {
77
+
std::cmp::Ordering::Equal => self.prefix.cmp(&other.prefix),
78
+
other => other,
79
+
}
80
+
}
81
+
}
82
+
83
+
impl std::hash::Hash for CompletionContext {
84
+
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
85
+
self.kind.hash(state);
86
+
self.prefix.hash(state);
87
+
}
88
+
}
+218
src/completion/variables.rs
+218
src/completion/variables.rs
···
1
+
use crate::console_log;
2
+
use nu_protocol::engine::{EngineState, Stack, StateWorkingSet};
3
+
use nu_protocol::{ENV_VARIABLE_ID, IN_VARIABLE_ID, NU_VARIABLE_ID, Span, Value};
4
+
use std::collections::HashMap;
5
+
6
+
pub fn eval_variable_for_completion(
7
+
var_id: nu_protocol::VarId,
8
+
working_set: &StateWorkingSet,
9
+
engine_guard: &EngineState,
10
+
stack_guard: &Stack,
11
+
) -> Option<Value> {
12
+
match var_id {
13
+
id if id == NU_VARIABLE_ID => {
14
+
// $nu - get from engine state constant
15
+
engine_guard.get_constant(id).cloned()
16
+
}
17
+
id if id == ENV_VARIABLE_ID => {
18
+
// $env - build from environment variables in engine state
19
+
// EnvVars is HashMap<String, HashMap<String, Value>> (overlay -> vars)
20
+
let mut pairs: Vec<(String, Value)> = Vec::new();
21
+
for overlay_env in engine_guard.env_vars.values() {
22
+
for (name, value) in overlay_env.iter() {
23
+
pairs.push((name.clone(), value.clone()));
24
+
}
25
+
}
26
+
pairs.sort_by(|a, b| a.0.cmp(&b.0));
27
+
// Deduplicate by name (later overlays override earlier ones)
28
+
pairs.dedup_by(|a, b| a.0 == b.0);
29
+
Some(Value::record(pairs.into_iter().collect(), Span::unknown()))
30
+
}
31
+
id if id == IN_VARIABLE_ID => {
32
+
// $in - typically not available at completion time
33
+
None
34
+
}
35
+
_ => {
36
+
// User-defined variable - try to get const value first
37
+
let var_info = working_set.get_variable(var_id);
38
+
if let Some(const_val) = &var_info.const_val {
39
+
Some(const_val.clone())
40
+
} else {
41
+
// Variable doesn't have a const value (runtime value)
42
+
// Try to get the value from the stack (runtime storage)
43
+
match stack_guard.get_var(var_id, Span::unknown()) {
44
+
Ok(value) => {
45
+
console_log!("[completion] Found variable {var_id:?} value in stack");
46
+
Some(value)
47
+
}
48
+
Err(_) => {
49
+
// Variable not in stack either
50
+
console_log!(
51
+
"[completion] Variable {var_id:?} has no const value and not in stack, type: {ty:?}",
52
+
ty = var_info.ty
53
+
);
54
+
None
55
+
}
56
+
}
57
+
}
58
+
}
59
+
}
60
+
}
61
+
62
+
pub fn get_columns_from_value(value: &Value) -> Vec<(String, Option<String>)> {
63
+
match value {
64
+
Value::Record { val, .. } => val
65
+
.iter()
66
+
.map(|(name, v)| (name.to_string(), Some(v.get_type().to_string())))
67
+
.collect(),
68
+
Value::List { vals, .. } => {
69
+
// Get common columns from list of records
70
+
if let Some(first) = vals.first() {
71
+
if let Value::Record { val, .. } = first {
72
+
return val
73
+
.iter()
74
+
.map(|(name, v)| (name.to_string(), Some(v.get_type().to_string())))
75
+
.collect();
76
+
}
77
+
}
78
+
vec![]
79
+
}
80
+
_ => vec![],
81
+
}
82
+
}
83
+
84
+
pub fn follow_cell_path(value: &Value, path: &[&str]) -> Option<Value> {
85
+
let mut current = value.clone();
86
+
for member in path {
87
+
match ¤t {
88
+
Value::Record { val, .. } => {
89
+
current = val.get(member)?.clone();
90
+
}
91
+
Value::List { vals, .. } => {
92
+
// Try to parse as index or get from first record
93
+
if let Ok(idx) = member.parse::<usize>() {
94
+
current = vals.get(idx)?.clone();
95
+
} else if let Some(first) = vals.first() {
96
+
if let Value::Record { val, .. } = first {
97
+
current = val.get(member)?.clone();
98
+
} else {
99
+
return None;
100
+
}
101
+
} else {
102
+
return None;
103
+
}
104
+
}
105
+
_ => return None,
106
+
}
107
+
}
108
+
Some(current)
109
+
}
110
+
111
+
pub fn extract_closure_params(input: &str, cursor_pos: usize) -> Vec<String> {
112
+
let mut params = Vec::new();
113
+
114
+
// Find all closures in the input by looking for {|...| patterns
115
+
// We need to find closures that contain the cursor position
116
+
let mut brace_stack: Vec<usize> = Vec::new(); // Stack of opening brace positions
117
+
let mut closures: Vec<(usize, usize, Vec<String>)> = Vec::new(); // (start, end, params)
118
+
119
+
let mut i = 0;
120
+
let chars: Vec<char> = input.chars().collect();
121
+
122
+
while i < chars.len() {
123
+
if chars[i] == '{' {
124
+
brace_stack.push(i);
125
+
} else if chars[i] == '}' {
126
+
if let Some(start) = brace_stack.pop() {
127
+
// Check if this is a closure with parameters: {|param| ...}
128
+
if start + 1 < chars.len() && chars[start + 1] == '|' {
129
+
// Find the parameter list
130
+
let param_start = start + 2;
131
+
let mut param_end = param_start;
132
+
133
+
// Find the closing | of the parameter list
134
+
while param_end < chars.len() && chars[param_end] != '|' {
135
+
param_end += 1;
136
+
}
137
+
138
+
if param_end < chars.len() {
139
+
// Extract parameter names
140
+
let params_text: String = chars[param_start..param_end].iter().collect();
141
+
let param_names: Vec<String> = params_text
142
+
.split(',')
143
+
.map(|s| s.trim().to_string())
144
+
.filter(|s| !s.is_empty())
145
+
.collect();
146
+
147
+
closures.push((start, i + 1, param_names));
148
+
}
149
+
}
150
+
}
151
+
}
152
+
i += 1;
153
+
}
154
+
155
+
// Find closures that contain the cursor position
156
+
// A closure contains the cursor if: start <= cursor_pos < end
157
+
for (start, end, param_names) in closures {
158
+
if start <= cursor_pos && cursor_pos < end {
159
+
console_log!(
160
+
"[completion] Found closure at [{start}, {end}) containing cursor {cursor_pos}, params: {param_names:?}"
161
+
);
162
+
params.extend(param_names);
163
+
}
164
+
}
165
+
166
+
params
167
+
}
168
+
169
+
pub fn collect_variables(
170
+
working_set: &StateWorkingSet,
171
+
input: &str,
172
+
cursor_pos: usize,
173
+
) -> HashMap<String, nu_protocol::VarId> {
174
+
let mut variables = HashMap::new();
175
+
176
+
// Add built-in variables
177
+
variables.insert("$nu".to_string(), NU_VARIABLE_ID);
178
+
variables.insert("$in".to_string(), IN_VARIABLE_ID);
179
+
variables.insert("$env".to_string(), ENV_VARIABLE_ID);
180
+
181
+
// Collect closure parameters at cursor position
182
+
// We don't need real var_ids for closure parameters since they're not evaluated yet
183
+
// We'll use a placeholder var_id (using IN_VARIABLE_ID as a safe placeholder)
184
+
// The actual var_id lookup will happen when the variable is used
185
+
let closure_params = extract_closure_params(input, cursor_pos);
186
+
for param_name in closure_params {
187
+
let var_name = format!("${}", param_name);
188
+
// Use IN_VARIABLE_ID as placeholder - it's safe since we're just using it for the name
189
+
// The completion logic only needs the name, not the actual var_id
190
+
variables.insert(var_name.clone(), IN_VARIABLE_ID);
191
+
console_log!("[completion] Added closure parameter: {var_name:?}");
192
+
}
193
+
194
+
// Collect from working set delta scope
195
+
let mut removed_overlays = vec![];
196
+
for scope_frame in working_set.delta.scope.iter().rev() {
197
+
for overlay_frame in scope_frame.active_overlays(&mut removed_overlays).rev() {
198
+
for (name, var_id) in &overlay_frame.vars {
199
+
let name = String::from_utf8_lossy(name).to_string();
200
+
variables.insert(name, *var_id);
201
+
}
202
+
}
203
+
}
204
+
205
+
// Collect from permanent state scope
206
+
for overlay_frame in working_set
207
+
.permanent_state
208
+
.active_overlays(&removed_overlays)
209
+
.rev()
210
+
{
211
+
for (name, var_id) in &overlay_frame.vars {
212
+
let name = String::from_utf8_lossy(name).to_string();
213
+
variables.insert(name, *var_id);
214
+
}
215
+
}
216
+
217
+
variables
218
+
}
-1061
src/completion.rs
-1061
src/completion.rs
···
1
-
use futures::FutureExt;
2
-
use js_sys::Promise;
3
-
use wasm_bindgen_futures::future_to_promise;
4
-
5
-
use super::*;
6
-
7
-
#[derive(Debug, Serialize)]
8
-
struct Suggestion {
9
-
name: String,
10
-
description: Option<String>,
11
-
is_command: bool,
12
-
rendered: String,
13
-
span_start: usize, // char index (not byte)
14
-
span_end: usize, // char index (not byte)
15
-
}
16
-
17
-
impl PartialEq for Suggestion {
18
-
fn eq(&self, other: &Self) -> bool {
19
-
self.name == other.name
20
-
}
21
-
}
22
-
impl Eq for Suggestion {}
23
-
impl PartialOrd for Suggestion {
24
-
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
25
-
self.name.partial_cmp(&other.name)
26
-
}
27
-
}
28
-
impl Ord for Suggestion {
29
-
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
30
-
self.name.cmp(&other.name)
31
-
}
32
-
}
33
-
34
-
#[wasm_bindgen]
35
-
pub fn completion(input: String, js_cursor_pos: usize) -> Promise {
36
-
future_to_promise(completion_impl(input, js_cursor_pos).map(|s| Ok(JsValue::from_str(&s))))
37
-
}
38
-
39
-
pub async fn completion_impl(input: String, js_cursor_pos: usize) -> String {
40
-
let engine_guard = read_engine_state().await;
41
-
let root = get_pwd();
42
-
43
-
// Map UTF-16 cursor position (from JS) to Byte index (for Rust)
44
-
let byte_pos = input
45
-
.char_indices()
46
-
.map(|(i, _)| i)
47
-
.nth(js_cursor_pos)
48
-
.unwrap_or(input.len());
49
-
50
-
let (working_set, shapes, global_offset) = {
51
-
let mut working_set = StateWorkingSet::new(&engine_guard);
52
-
let global_offset = working_set.next_span_start();
53
-
let block = parse(&mut working_set, None, input.as_bytes(), false);
54
-
let shapes = flatten_block(&working_set, &block);
55
-
(working_set, shapes, global_offset)
56
-
};
57
-
58
-
// Initial state logging
59
-
web_sys::console::log_1(&JsValue::from_str(&format!(
60
-
"[completion] Input: {:?}, JS cursor: {}, byte cursor: {}",
61
-
input, js_cursor_pos, byte_pos
62
-
)));
63
-
web_sys::console::log_1(&JsValue::from_str(&format!(
64
-
"[completion] Found {} shapes, global_offset: {}",
65
-
shapes.len(),
66
-
global_offset
67
-
)));
68
-
for (idx, (span, shape)) in shapes.iter().enumerate() {
69
-
let (local_start, local_end) = (
70
-
span.start.saturating_sub(global_offset),
71
-
span.end.saturating_sub(global_offset),
72
-
);
73
-
web_sys::console::log_1(&JsValue::from_str(&format!(
74
-
"[completion] Shape {}: {:?} at [{}, {}] (local: [{}, {}])",
75
-
idx, shape, span.start, span.end, local_start, local_end
76
-
)));
77
-
}
78
-
79
-
// Helper functions
80
-
let is_separator_char = |c: char| -> bool { ['|', ';', '(', '{'].contains(&c) };
81
-
82
-
let is_command_separator_char = |c: char| -> bool { ['|', ';'].contains(&c) };
83
-
84
-
let has_separator_between = |start: usize, end: usize| -> bool {
85
-
if start < end && start < input.len() {
86
-
let text_between = &input[start..std::cmp::min(end, input.len())];
87
-
text_between.chars().any(|c| is_separator_char(c))
88
-
} else {
89
-
false
90
-
}
91
-
};
92
-
93
-
let find_last_separator_pos = |text: &str| -> Option<usize> {
94
-
text.rfind(|c| is_command_separator_char(c)).map(|i| i + 1)
95
-
};
96
-
97
-
let ends_with_separator = |text: &str| -> bool {
98
-
let text = text.trim_end();
99
-
text.ends_with('|') || text.ends_with(';')
100
-
};
101
-
102
-
let to_local_span = |span: Span| -> Span {
103
-
Span::new(
104
-
span.start.saturating_sub(global_offset),
105
-
span.end.saturating_sub(global_offset),
106
-
)
107
-
};
108
-
109
-
let safe_slice = |span: Span| -> String {
110
-
(span.start < input.len())
111
-
.then(|| {
112
-
let safe_end = std::cmp::min(span.end, input.len());
113
-
input[span.start..safe_end].to_string()
114
-
})
115
-
.unwrap_or_default()
116
-
};
117
-
118
-
let is_command_shape = |shape: &FlatShape, local_span: Span| -> bool {
119
-
matches!(
120
-
shape,
121
-
FlatShape::External(_) | FlatShape::InternalCall(_) | FlatShape::Keyword
122
-
) || matches!(shape, FlatShape::Garbage) && {
123
-
if local_span.start < input.len() {
124
-
let prev_text = &safe_slice(local_span);
125
-
!prev_text.trim().starts_with('-')
126
-
} else {
127
-
false
128
-
}
129
-
}
130
-
};
131
-
132
-
let handle_block_prefix = |prefix: &str, span: Span| -> Option<(String, Span, bool)> {
133
-
let mut block_prefix = prefix;
134
-
let mut block_span_start = span.start;
135
-
136
-
// Remove leading '{' and whitespace
137
-
if block_prefix.starts_with('{') {
138
-
block_prefix = &block_prefix[1..];
139
-
block_span_start += 1;
140
-
}
141
-
let trimmed_block_prefix = block_prefix.trim_start();
142
-
if trimmed_block_prefix != block_prefix {
143
-
// Adjust span start to skip whitespace
144
-
block_span_start += block_prefix.len() - trimmed_block_prefix.len();
145
-
}
146
-
147
-
let is_empty = trimmed_block_prefix.is_empty();
148
-
Some((
149
-
trimmed_block_prefix.to_string(),
150
-
Span::new(block_span_start, span.end),
151
-
is_empty,
152
-
))
153
-
};
154
-
155
-
// Helper function to find command name and count arguments before cursor
156
-
let find_command_and_arg_index =
157
-
|current_idx: usize, current_local_span: Span| -> Option<(String, usize)> {
158
-
let mut command_name: Option<String> = None;
159
-
let mut arg_count = 0;
160
-
161
-
// Look backwards through shapes to find the command
162
-
for i in (0..current_idx).rev() {
163
-
if let Some((prev_span, prev_shape)) = shapes.get(i) {
164
-
let prev_local_span = to_local_span(*prev_span);
165
-
166
-
// Check if there's a separator between this shape and the next one
167
-
let next_shape_start = if i + 1 < shapes.len() {
168
-
to_local_span(shapes[i + 1].0).start
169
-
} else {
170
-
current_local_span.start
171
-
};
172
-
173
-
if has_separator_between(prev_local_span.end, next_shape_start) {
174
-
break; // Stop at separator
175
-
}
176
-
177
-
if is_command_shape(prev_shape, prev_local_span) {
178
-
// Found the command
179
-
let cmd_text = safe_slice(prev_local_span);
180
-
// Extract just the command name (first word, no flags)
181
-
let cmd_name = cmd_text
182
-
.split_whitespace()
183
-
.next()
184
-
.unwrap_or(&cmd_text)
185
-
.trim();
186
-
command_name = Some(cmd_name.to_string());
187
-
break;
188
-
} else {
189
-
// This is an argument - count it if it's not a flag
190
-
let arg_text = safe_slice(prev_local_span);
191
-
let trimmed_arg = arg_text.trim();
192
-
// Don't count flags (starting with -) or empty arguments
193
-
if !trimmed_arg.is_empty() && !trimmed_arg.starts_with('-') {
194
-
arg_count += 1;
195
-
}
196
-
}
197
-
}
198
-
}
199
-
200
-
command_name.map(|name| (name, arg_count))
201
-
};
202
-
203
-
// Helper function to handle both Block and Closure shapes
204
-
let handle_block_or_closure = |prefix: &str,
205
-
span: Span,
206
-
shape_name: &str,
207
-
current_idx: usize,
208
-
local_span: Span|
209
-
-> Option<CompletionContext> {
210
-
web_sys::console::log_1(&JsValue::from_str(&format!(
211
-
"[completion] Processing {} shape with prefix: {:?}",
212
-
shape_name, prefix
213
-
)));
214
-
215
-
// Check if the content ends with a pipe or semicolon
216
-
let prefix_ends_with_separator = ends_with_separator(prefix);
217
-
let last_sep_pos_in_prefix = if prefix_ends_with_separator {
218
-
find_last_separator_pos(prefix)
219
-
} else {
220
-
None
221
-
};
222
-
web_sys::console::log_1(&JsValue::from_str(&format!(
223
-
"[completion] {}: prefix_ends_with_separator={}, last_sep_pos_in_prefix={:?}",
224
-
shape_name, prefix_ends_with_separator, last_sep_pos_in_prefix
225
-
)));
226
-
227
-
if let Some((trimmed_prefix, adjusted_span, is_empty)) = handle_block_prefix(prefix, span) {
228
-
web_sys::console::log_1(&JsValue::from_str(&format!(
229
-
"[completion] {}: trimmed_prefix={:?}, is_empty={}",
230
-
shape_name, trimmed_prefix, is_empty
231
-
)));
232
-
233
-
if is_empty {
234
-
// Empty block/closure or just whitespace - command context
235
-
web_sys::console::log_1(&JsValue::from_str(&format!(
236
-
"[completion] {} is empty, setting Command context",
237
-
shape_name
238
-
)));
239
-
Some(CompletionContext::Command {
240
-
prefix: String::new(),
241
-
span: adjusted_span,
242
-
})
243
-
} else if let Some(last_sep_pos) = last_sep_pos_in_prefix {
244
-
// After a separator - command context
245
-
let after_sep = prefix[last_sep_pos..].trim_start();
246
-
web_sys::console::log_1(&JsValue::from_str(&format!(
247
-
"[completion] {} has separator at {}, after_sep={:?}, setting Command context",
248
-
shape_name, last_sep_pos, after_sep
249
-
)));
250
-
Some(CompletionContext::Command {
251
-
prefix: after_sep.to_string(),
252
-
span: Span::new(span.start + last_sep_pos, span.end),
253
-
})
254
-
} else {
255
-
web_sys::console::log_1(&JsValue::from_str(&format!(
256
-
"[completion] {} has no separator, checking for flag/argument context",
257
-
shape_name
258
-
)));
259
-
// Check if this is a flag or command argument
260
-
let trimmed = trimmed_prefix.trim();
261
-
let is_flag = trimmed.starts_with('-');
262
-
263
-
// Try to find the command and argument index
264
-
if let Some((cmd_name, arg_index)) =
265
-
find_command_and_arg_index(current_idx, local_span)
266
-
{
267
-
if is_flag {
268
-
web_sys::console::log_1(&JsValue::from_str(&format!(
269
-
"[completion] {}: Found command {:?} for flag completion",
270
-
shape_name, cmd_name
271
-
)));
272
-
Some(CompletionContext::Flag {
273
-
prefix: trimmed.to_string(),
274
-
span: adjusted_span,
275
-
command_name: cmd_name,
276
-
})
277
-
} else {
278
-
web_sys::console::log_1(&JsValue::from_str(&format!(
279
-
"[completion] {}: Found command {:?} with arg_index {} for argument completion",
280
-
shape_name, cmd_name, arg_index
281
-
)));
282
-
Some(CompletionContext::CommandArgument {
283
-
prefix: trimmed.to_string(),
284
-
span: adjusted_span,
285
-
command_name: cmd_name,
286
-
arg_index,
287
-
})
288
-
}
289
-
} else {
290
-
// No command found, treat as regular argument
291
-
web_sys::console::log_1(&JsValue::from_str(&format!(
292
-
"[completion] {}: No command found{}, using Argument context",
293
-
shape_name,
294
-
if is_flag { " for flag" } else { "" }
295
-
)));
296
-
Some(CompletionContext::Argument {
297
-
prefix: trimmed_prefix,
298
-
span: adjusted_span,
299
-
})
300
-
}
301
-
}
302
-
} else {
303
-
None
304
-
}
305
-
};
306
-
307
-
// Find what we're completing
308
-
#[derive(Debug)]
309
-
enum CompletionContext {
310
-
Command {
311
-
prefix: String,
312
-
span: Span,
313
-
},
314
-
Argument {
315
-
prefix: String,
316
-
span: Span,
317
-
},
318
-
Flag {
319
-
prefix: String,
320
-
span: Span,
321
-
command_name: String,
322
-
},
323
-
CommandArgument {
324
-
prefix: String,
325
-
span: Span,
326
-
command_name: String,
327
-
arg_index: usize,
328
-
},
329
-
}
330
-
331
-
let mut context: Option<CompletionContext> = None;
332
-
333
-
// Helper function to build full command prefix by looking backwards through shapes
334
-
let build_command_prefix =
335
-
|current_idx: usize, current_local_span: Span, current_prefix: &str| -> (String, Span) {
336
-
let mut span_start = current_local_span.start;
337
-
338
-
// Look backwards through shapes to find previous command words
339
-
for i in (0..current_idx).rev() {
340
-
if let Some((prev_span, prev_shape)) = shapes.get(i) {
341
-
let prev_local_span = to_local_span(*prev_span);
342
-
343
-
if is_command_shape(prev_shape, prev_local_span) {
344
-
// Check if there's a separator between this shape and the next one
345
-
let next_shape_start = if i + 1 < shapes.len() {
346
-
to_local_span(shapes[i + 1].0).start
347
-
} else {
348
-
current_local_span.start
349
-
};
350
-
351
-
// Check if there's a separator (pipe, semicolon, etc.) between shapes
352
-
// Whitespace is fine, but separators indicate a new command
353
-
if has_separator_between(prev_local_span.end, next_shape_start) {
354
-
break; // Stop at separator
355
-
}
356
-
357
-
// Update span start to include this command word
358
-
span_start = prev_local_span.start;
359
-
} else {
360
-
// Not a command shape, stop looking backwards
361
-
break;
362
-
}
363
-
}
364
-
}
365
-
366
-
// Extract the full prefix from the input, preserving exact spacing
367
-
let span_end = current_local_span.end;
368
-
let full_prefix = if span_start < input.len() {
369
-
safe_slice(Span::new(span_start, span_end))
370
-
} else {
371
-
current_prefix.to_string()
372
-
};
373
-
374
-
(full_prefix, Span::new(span_start, span_end))
375
-
};
376
-
377
-
// First, check if cursor is within a shape
378
-
for (idx, (span, shape)) in shapes.iter().enumerate() {
379
-
let local_span = to_local_span(*span);
380
-
381
-
if local_span.start <= byte_pos && byte_pos <= local_span.end {
382
-
web_sys::console::log_1(&JsValue::from_str(&format!(
383
-
"[completion] Cursor in shape {}: {:?} at {:?}",
384
-
idx, shape, local_span
385
-
)));
386
-
387
-
// Check if there's a pipe or semicolon between this shape's end and the cursor
388
-
// If so, we're starting a new command and should ignore this shape
389
-
let has_sep = has_separator_between(local_span.end, byte_pos);
390
-
if has_sep {
391
-
web_sys::console::log_1(&JsValue::from_str(&format!(
392
-
"[completion] Separator found between shape end ({}) and cursor ({}), skipping shape",
393
-
local_span.end, byte_pos
394
-
)));
395
-
// There's a separator, so we're starting a new command - skip this shape
396
-
continue;
397
-
}
398
-
399
-
let span = Span::new(local_span.start, std::cmp::min(local_span.end, byte_pos));
400
-
let prefix = safe_slice(span);
401
-
web_sys::console::log_1(&JsValue::from_str(&format!(
402
-
"[completion] Processing shape {} with prefix: {:?}",
403
-
idx, prefix
404
-
)));
405
-
406
-
// Special case: if prefix is just '{' (possibly with whitespace),
407
-
// we're at the start of a block and should complete commands
408
-
let trimmed_prefix = prefix.trim();
409
-
if trimmed_prefix == "{" {
410
-
// We're right after '{' - command context
411
-
if let Some((_, adjusted_span, _)) = handle_block_prefix(&prefix, span) {
412
-
context = Some(CompletionContext::Command {
413
-
prefix: String::new(),
414
-
span: adjusted_span,
415
-
});
416
-
}
417
-
} else {
418
-
match shape {
419
-
_ if is_command_shape(shape, local_span) => {
420
-
let (full_prefix, full_span) = build_command_prefix(idx, span, &prefix);
421
-
context = Some(CompletionContext::Command {
422
-
prefix: full_prefix,
423
-
span: full_span,
424
-
});
425
-
}
426
-
FlatShape::Block | FlatShape::Closure => {
427
-
if let Some(ctx) = handle_block_or_closure(
428
-
&prefix,
429
-
span,
430
-
shape.as_str().trim_start_matches("shape_"),
431
-
idx,
432
-
local_span,
433
-
) {
434
-
context = Some(ctx);
435
-
}
436
-
}
437
-
_ => {
438
-
// Check if this is a flag or command argument
439
-
let trimmed_prefix = prefix.trim();
440
-
if trimmed_prefix.starts_with('-') {
441
-
// This looks like a flag - find the command
442
-
if let Some((cmd_name, _)) = find_command_and_arg_index(idx, local_span)
443
-
{
444
-
context = Some(CompletionContext::Flag {
445
-
prefix: trimmed_prefix.to_string(),
446
-
span,
447
-
command_name: cmd_name,
448
-
});
449
-
} else {
450
-
context = Some(CompletionContext::Argument { prefix, span });
451
-
}
452
-
} else {
453
-
// This is a positional argument - find the command and argument index
454
-
if let Some((cmd_name, arg_index)) =
455
-
find_command_and_arg_index(idx, local_span)
456
-
{
457
-
context = Some(CompletionContext::CommandArgument {
458
-
prefix: trimmed_prefix.to_string(),
459
-
span,
460
-
command_name: cmd_name,
461
-
arg_index,
462
-
});
463
-
} else {
464
-
context = Some(CompletionContext::Argument { prefix, span });
465
-
}
466
-
}
467
-
}
468
-
}
469
-
}
470
-
break;
471
-
}
472
-
}
473
-
474
-
// If not in a shape, check what comes before the cursor
475
-
if context.is_none() {
476
-
web_sys::console::log_1(&JsValue::from_str(
477
-
"[completion] Context is None, entering fallback logic",
478
-
));
479
-
// Check if there's a command-like shape before us
480
-
let mut found_command_before = false;
481
-
let mut has_separator_after_command = false;
482
-
for (span, shape) in shapes.iter().rev() {
483
-
let local_span = to_local_span(*span);
484
-
if local_span.end <= byte_pos {
485
-
if is_command_shape(shape, local_span) {
486
-
// Check if there's a pipe or semicolon between this command and the cursor
487
-
has_separator_after_command = has_separator_between(local_span.end, byte_pos);
488
-
web_sys::console::log_1(&JsValue::from_str(&format!(
489
-
"[completion] Found command shape {:?} at {:?}, has_separator_after_command={}",
490
-
shape, local_span, has_separator_after_command
491
-
)));
492
-
if !has_separator_after_command {
493
-
found_command_before = true;
494
-
495
-
// Extract the command text
496
-
let cmd = safe_slice(local_span);
497
-
web_sys::console::log_1(&JsValue::from_str(&format!(
498
-
"[completion] Set Command context with prefix: {:?}",
499
-
cmd
500
-
)));
501
-
502
-
// We're after a command, complete with that command as prefix
503
-
context = Some(CompletionContext::Command {
504
-
prefix: cmd,
505
-
span: local_span,
506
-
});
507
-
}
508
-
}
509
-
break;
510
-
}
511
-
}
512
-
513
-
if !found_command_before {
514
-
web_sys::console::log_1(&JsValue::from_str(
515
-
"[completion] No command found before cursor, checking tokens",
516
-
));
517
-
// No command before, check context from tokens
518
-
let (tokens, _) = lex(input.as_bytes(), 0, &[], &[], true);
519
-
let last_token = tokens.iter().filter(|t| t.span.end <= byte_pos).last();
520
-
521
-
let is_cmd_context = if let Some(token) = last_token {
522
-
let matches = matches!(
523
-
token.contents,
524
-
TokenContents::Pipe
525
-
| TokenContents::PipePipe
526
-
| TokenContents::Semicolon
527
-
| TokenContents::Eol
528
-
);
529
-
web_sys::console::log_1(&JsValue::from_str(&format!(
530
-
"[completion] Last token: {:?}, is_cmd_context from token={}",
531
-
token.contents, matches
532
-
)));
533
-
matches
534
-
} else {
535
-
web_sys::console::log_1(&JsValue::from_str(
536
-
"[completion] No last token found, assuming start of input (is_cmd_context=true)",
537
-
));
538
-
true // Start of input
539
-
};
540
-
541
-
// Look for the last non-whitespace token before cursor
542
-
let text_before = &input[..byte_pos];
543
-
544
-
// Also check if we're inside a block - if the last non-whitespace char before cursor is '{'
545
-
let text_before_trimmed = text_before.trim_end();
546
-
let is_inside_block = text_before_trimmed.ends_with('{');
547
-
// If we found a separator after a command, we're starting a new command
548
-
let is_cmd_context = is_cmd_context || is_inside_block || has_separator_after_command;
549
-
web_sys::console::log_1(&JsValue::from_str(&format!(
550
-
"[completion] is_inside_block={}, has_separator_after_command={}, final is_cmd_context={}",
551
-
is_inside_block, has_separator_after_command, is_cmd_context
552
-
)));
553
-
554
-
// Find the last word before cursor
555
-
let last_word_start = text_before
556
-
.rfind(|c: char| c.is_whitespace() || is_separator_char(c))
557
-
.map(|i| i + 1)
558
-
.unwrap_or(0);
559
-
560
-
let last_word = text_before[last_word_start..].trim_start();
561
-
web_sys::console::log_1(&JsValue::from_str(&format!(
562
-
"[completion] last_word_start={}, last_word={:?}",
563
-
last_word_start, last_word
564
-
)));
565
-
566
-
if is_cmd_context {
567
-
context = Some(CompletionContext::Command {
568
-
prefix: last_word.to_string(),
569
-
span: Span::new(last_word_start, byte_pos),
570
-
});
571
-
web_sys::console::log_1(&JsValue::from_str(&format!(
572
-
"[completion] Set Command context with prefix: {:?}",
573
-
last_word
574
-
)));
575
-
} else {
576
-
// Check if this is a flag or command argument
577
-
let trimmed_word = last_word.trim();
578
-
if trimmed_word.starts_with('-') {
579
-
// Try to find command by looking backwards through shapes
580
-
let mut found_cmd = None;
581
-
for (span, shape) in shapes.iter().rev() {
582
-
let local_span = to_local_span(*span);
583
-
if local_span.end <= byte_pos && is_command_shape(shape, local_span) {
584
-
let cmd_text = safe_slice(local_span);
585
-
let cmd_name = cmd_text
586
-
.split_whitespace()
587
-
.next()
588
-
.unwrap_or(&cmd_text)
589
-
.trim();
590
-
found_cmd = Some(cmd_name.to_string());
591
-
break;
592
-
}
593
-
}
594
-
if let Some(cmd_name) = found_cmd {
595
-
let cmd_name_clone = cmd_name.clone();
596
-
context = Some(CompletionContext::Flag {
597
-
prefix: trimmed_word.to_string(),
598
-
span: Span::new(last_word_start, byte_pos),
599
-
command_name: cmd_name,
600
-
});
601
-
web_sys::console::log_1(&JsValue::from_str(&format!(
602
-
"[completion] Set Flag context with prefix: {:?}, command: {:?}",
603
-
trimmed_word, cmd_name_clone
604
-
)));
605
-
} else {
606
-
context = Some(CompletionContext::Argument {
607
-
prefix: last_word.to_string(),
608
-
span: Span::new(last_word_start, byte_pos),
609
-
});
610
-
web_sys::console::log_1(&JsValue::from_str(&format!(
611
-
"[completion] Set Argument context with prefix: {:?}",
612
-
last_word
613
-
)));
614
-
}
615
-
} else {
616
-
// Try to find command and argument index
617
-
let mut found_cmd = None;
618
-
let mut arg_count = 0;
619
-
for (span, shape) in shapes.iter().rev() {
620
-
let local_span = to_local_span(*span);
621
-
if local_span.end <= byte_pos {
622
-
if is_command_shape(shape, local_span) {
623
-
let cmd_text = safe_slice(local_span);
624
-
let cmd_name = cmd_text
625
-
.split_whitespace()
626
-
.next()
627
-
.unwrap_or(&cmd_text)
628
-
.trim();
629
-
found_cmd = Some(cmd_name.to_string());
630
-
break;
631
-
} else {
632
-
let arg_text = safe_slice(local_span);
633
-
let trimmed_arg = arg_text.trim();
634
-
if !trimmed_arg.is_empty() && !trimmed_arg.starts_with('-') {
635
-
arg_count += 1;
636
-
}
637
-
}
638
-
}
639
-
}
640
-
if let Some(cmd_name) = found_cmd {
641
-
let cmd_name_clone = cmd_name.clone();
642
-
context = Some(CompletionContext::CommandArgument {
643
-
prefix: trimmed_word.to_string(),
644
-
span: Span::new(last_word_start, byte_pos),
645
-
command_name: cmd_name,
646
-
arg_index: arg_count,
647
-
});
648
-
web_sys::console::log_1(&JsValue::from_str(&format!(
649
-
"[completion] Set CommandArgument context with prefix: {:?}, command: {:?}, arg_index: {}",
650
-
trimmed_word, cmd_name_clone, arg_count
651
-
)));
652
-
} else {
653
-
context = Some(CompletionContext::Argument {
654
-
prefix: last_word.to_string(),
655
-
span: Span::new(last_word_start, byte_pos),
656
-
});
657
-
web_sys::console::log_1(&JsValue::from_str(&format!(
658
-
"[completion] Set Argument context with prefix: {:?}",
659
-
last_word
660
-
)));
661
-
}
662
-
}
663
-
}
664
-
}
665
-
}
666
-
667
-
web_sys::console::log_1(&JsValue::from_str(&format!("context: {:?}", context)));
668
-
669
-
let mut suggestions: Vec<Suggestion> = Vec::new();
670
-
671
-
// Convert byte-spans back to char-spans for JS
672
-
let to_char_span = |span: Span| -> Span {
673
-
let char_start = input[..span.start].chars().count();
674
-
let char_end = input[..span.end].chars().count();
675
-
Span::new(char_start, char_end)
676
-
};
677
-
678
-
let get_command_signature = |cmd_name: &str| -> Option<nu_protocol::Signature> {
679
-
engine_guard
680
-
.find_decl(cmd_name.as_bytes(), &[])
681
-
.map(|id| engine_guard.get_decl(id).signature())
682
-
};
683
-
684
-
match context {
685
-
Some(CompletionContext::Command { prefix, span }) => {
686
-
web_sys::console::log_1(&JsValue::from_str(&format!(
687
-
"[completion] Generating Command suggestions with prefix: {:?}",
688
-
prefix
689
-
)));
690
-
// Command completion
691
-
let cmds = working_set
692
-
.find_commands_by_predicate(|value| value.starts_with(prefix.as_bytes()), true);
693
-
694
-
let span = to_char_span(span);
695
-
let mut cmd_count = 0;
696
-
697
-
for (_, name, desc, _) in cmds {
698
-
let name_str = String::from_utf8_lossy(&name).to_string();
699
-
suggestions.push(Suggestion {
700
-
rendered: {
701
-
let name_colored = ansi_term::Color::Green.bold().paint(&name_str);
702
-
let desc_str = desc.as_deref().unwrap_or("<no description>");
703
-
format!("{name_colored} {desc_str}")
704
-
},
705
-
name: name_str,
706
-
description: desc,
707
-
is_command: true,
708
-
span_start: span.start,
709
-
span_end: span.end,
710
-
});
711
-
cmd_count += 1;
712
-
}
713
-
web_sys::console::log_1(&JsValue::from_str(&format!(
714
-
"[completion] Found {} command suggestions",
715
-
cmd_count
716
-
)));
717
-
}
718
-
Some(CompletionContext::Argument { prefix, span }) => {
719
-
web_sys::console::log_1(&JsValue::from_str(&format!(
720
-
"[completion] Generating Argument suggestions with prefix: {:?}",
721
-
prefix
722
-
)));
723
-
// File completion
724
-
let (dir, file_prefix) = prefix
725
-
.rfind('/')
726
-
.map(|idx| (&prefix[..idx + 1], &prefix[idx + 1..]))
727
-
.unwrap_or(("", prefix.as_str()));
728
-
729
-
let dir_to_join = (dir.len() > 1 && dir.ends_with('/'))
730
-
.then(|| &dir[..dir.len() - 1])
731
-
.unwrap_or(dir);
732
-
733
-
let target_dir = if !dir.is_empty() {
734
-
match root.join(dir_to_join) {
735
-
Ok(d) if d.is_dir().unwrap_or(false) => Some(d),
736
-
_ => None,
737
-
}
738
-
} else {
739
-
Some(root.join("").unwrap())
740
-
};
741
-
742
-
let mut file_count = 0;
743
-
if let Some(d) = target_dir {
744
-
if let Ok(iterator) = d.read_dir() {
745
-
let span = to_char_span(span);
746
-
747
-
for entry in iterator {
748
-
let name = entry.filename();
749
-
if name.starts_with(file_prefix) {
750
-
let full_completion = format!("{}{}", dir, name);
751
-
suggestions.push(Suggestion {
752
-
name: full_completion.clone(),
753
-
description: None,
754
-
is_command: false,
755
-
rendered: full_completion,
756
-
span_start: span.start,
757
-
span_end: span.end,
758
-
});
759
-
file_count += 1;
760
-
}
761
-
}
762
-
}
763
-
}
764
-
web_sys::console::log_1(&JsValue::from_str(&format!(
765
-
"[completion] Found {} file suggestions",
766
-
file_count
767
-
)));
768
-
}
769
-
Some(CompletionContext::Flag {
770
-
prefix,
771
-
span,
772
-
command_name,
773
-
}) => {
774
-
web_sys::console::log_1(&JsValue::from_str(&format!(
775
-
"[completion] Generating Flag suggestions for command: {:?}, prefix: {:?}",
776
-
command_name, prefix
777
-
)));
778
-
779
-
if let Some(signature) = get_command_signature(&command_name) {
780
-
let span = to_char_span(span);
781
-
let mut flag_count = 0;
782
-
783
-
// Get switches from signature
784
-
// Signature has a named field that contains named arguments (including switches)
785
-
for flag in &signature.named {
786
-
// Check if this is a switch (has no argument)
787
-
// Switches have arg: None, named arguments have arg: Some(SyntaxShape)
788
-
let is_switch = flag.arg.is_none();
789
-
790
-
if is_switch {
791
-
let long_name = format!("--{}", flag.long);
792
-
let short_name = flag.short.map(|c| format!("-{}", c));
793
-
794
-
// Check if prefix matches long or short form
795
-
let matches_long = long_name.starts_with(&prefix) || prefix.is_empty();
796
-
let matches_short = short_name
797
-
.as_ref()
798
-
.map(|s| s.starts_with(&prefix) || prefix.is_empty())
799
-
.unwrap_or(false);
800
-
801
-
if matches_long {
802
-
suggestions.push(Suggestion {
803
-
name: long_name.clone(),
804
-
description: Some(flag.desc.clone()),
805
-
is_command: false,
806
-
rendered: {
807
-
let flag_colored =
808
-
ansi_term::Color::Cyan.bold().paint(&long_name);
809
-
format!("{flag_colored} {}", flag.desc)
810
-
},
811
-
span_start: span.start,
812
-
span_end: span.end,
813
-
});
814
-
flag_count += 1;
815
-
}
816
-
817
-
if matches_short {
818
-
if let Some(short) = short_name {
819
-
suggestions.push(Suggestion {
820
-
name: short.clone(),
821
-
description: Some(flag.desc.clone()),
822
-
is_command: false,
823
-
rendered: {
824
-
let flag_colored =
825
-
ansi_term::Color::Cyan.bold().paint(&short);
826
-
format!("{flag_colored} {}", flag.desc)
827
-
},
828
-
span_start: span.start,
829
-
span_end: span.end,
830
-
});
831
-
flag_count += 1;
832
-
}
833
-
}
834
-
}
835
-
}
836
-
837
-
web_sys::console::log_1(&JsValue::from_str(&format!(
838
-
"[completion] Found {} flag suggestions",
839
-
flag_count
840
-
)));
841
-
} else {
842
-
web_sys::console::log_1(&JsValue::from_str(&format!(
843
-
"[completion] Could not find signature for command: {:?}",
844
-
command_name
845
-
)));
846
-
}
847
-
}
848
-
Some(CompletionContext::CommandArgument {
849
-
prefix,
850
-
span,
851
-
command_name,
852
-
arg_index,
853
-
}) => {
854
-
web_sys::console::log_1(&JsValue::from_str(&format!(
855
-
"[completion] Generating CommandArgument suggestions for command: {:?}, arg_index: {}, prefix: {:?}",
856
-
command_name, arg_index, prefix
857
-
)));
858
-
859
-
if let Some(signature) = get_command_signature(&command_name) {
860
-
// Get positional arguments from signature
861
-
// Combine required and optional positional arguments
862
-
let mut all_positional = Vec::new();
863
-
all_positional.extend_from_slice(&signature.required_positional);
864
-
all_positional.extend_from_slice(&signature.optional_positional);
865
-
866
-
// Find the argument at the given index
867
-
if let Some(arg) = all_positional.get(arg_index) {
868
-
// Check the SyntaxShape to determine completion type
869
-
match &arg.shape {
870
-
nu_protocol::SyntaxShape::String | nu_protocol::SyntaxShape::Filepath => {
871
-
// File/directory completion
872
-
let (dir, file_prefix) = prefix
873
-
.rfind('/')
874
-
.map(|idx| (&prefix[..idx + 1], &prefix[idx + 1..]))
875
-
.unwrap_or(("", prefix.as_str()));
876
-
877
-
let dir_to_join = (dir.len() > 1 && dir.ends_with('/'))
878
-
.then(|| &dir[..dir.len() - 1])
879
-
.unwrap_or(dir);
880
-
881
-
let target_dir = if !dir.is_empty() {
882
-
match root.join(dir_to_join) {
883
-
Ok(d) if d.is_dir().unwrap_or(false) => Some(d),
884
-
_ => None,
885
-
}
886
-
} else {
887
-
Some(root.join("").unwrap())
888
-
};
889
-
890
-
let span = to_char_span(span);
891
-
let mut file_count = 0;
892
-
if let Some(d) = target_dir {
893
-
if let Ok(iterator) = d.read_dir() {
894
-
for entry in iterator {
895
-
let name = entry.filename();
896
-
if name.starts_with(file_prefix) {
897
-
let full_completion = format!("{}{}", dir, name);
898
-
suggestions.push(Suggestion {
899
-
name: full_completion.clone(),
900
-
description: Some(arg.desc.clone()),
901
-
is_command: false,
902
-
rendered: full_completion,
903
-
span_start: span.start,
904
-
span_end: span.end,
905
-
});
906
-
file_count += 1;
907
-
}
908
-
}
909
-
}
910
-
}
911
-
web_sys::console::log_1(&JsValue::from_str(&format!(
912
-
"[completion] Found {} file suggestions for argument {}",
913
-
file_count, arg_index
914
-
)));
915
-
}
916
-
_ => {
917
-
// For other types, fall back to file completion
918
-
let (dir, file_prefix) = prefix
919
-
.rfind('/')
920
-
.map(|idx| (&prefix[..idx + 1], &prefix[idx + 1..]))
921
-
.unwrap_or(("", prefix.as_str()));
922
-
923
-
let dir_to_join = (dir.len() > 1 && dir.ends_with('/'))
924
-
.then(|| &dir[..dir.len() - 1])
925
-
.unwrap_or(dir);
926
-
927
-
let target_dir = if !dir.is_empty() {
928
-
match root.join(dir_to_join) {
929
-
Ok(d) if d.is_dir().unwrap_or(false) => Some(d),
930
-
_ => None,
931
-
}
932
-
} else {
933
-
Some(root.join("").unwrap())
934
-
};
935
-
936
-
let span = to_char_span(span);
937
-
if let Some(d) = target_dir {
938
-
if let Ok(iterator) = d.read_dir() {
939
-
for entry in iterator {
940
-
let name = entry.filename();
941
-
if name.starts_with(file_prefix) {
942
-
let full_completion = format!("{}{}", dir, name);
943
-
suggestions.push(Suggestion {
944
-
name: full_completion.clone(),
945
-
description: Some(arg.desc.clone()),
946
-
is_command: false,
947
-
rendered: full_completion,
948
-
span_start: span.start,
949
-
span_end: span.end,
950
-
});
951
-
}
952
-
}
953
-
}
954
-
}
955
-
}
956
-
}
957
-
} else {
958
-
// Argument index out of range, fall back to file completion
959
-
web_sys::console::log_1(&JsValue::from_str(&format!(
960
-
"[completion] Argument index {} out of range, using file completion",
961
-
arg_index
962
-
)));
963
-
// Use the same file completion logic as Argument context
964
-
let (dir, file_prefix) = prefix
965
-
.rfind('/')
966
-
.map(|idx| (&prefix[..idx + 1], &prefix[idx + 1..]))
967
-
.unwrap_or(("", prefix.as_str()));
968
-
969
-
let dir_to_join = (dir.len() > 1 && dir.ends_with('/'))
970
-
.then(|| &dir[..dir.len() - 1])
971
-
.unwrap_or(dir);
972
-
973
-
let target_dir = if !dir.is_empty() {
974
-
match root.join(dir_to_join) {
975
-
Ok(d) if d.is_dir().unwrap_or(false) => Some(d),
976
-
_ => None,
977
-
}
978
-
} else {
979
-
Some(root.join("").unwrap())
980
-
};
981
-
982
-
let span = to_char_span(span);
983
-
if let Some(d) = target_dir {
984
-
if let Ok(iterator) = d.read_dir() {
985
-
for entry in iterator {
986
-
let name = entry.filename();
987
-
if name.starts_with(file_prefix) {
988
-
let full_completion = format!("{}{}", dir, name);
989
-
suggestions.push(Suggestion {
990
-
name: full_completion.clone(),
991
-
description: None,
992
-
is_command: false,
993
-
rendered: full_completion,
994
-
span_start: span.start,
995
-
span_end: span.end,
996
-
});
997
-
}
998
-
}
999
-
}
1000
-
}
1001
-
}
1002
-
} else {
1003
-
// No signature found, fall back to file completion
1004
-
web_sys::console::log_1(&JsValue::from_str(&format!(
1005
-
"[completion] Could not find signature for command: {:?}, using file completion",
1006
-
command_name
1007
-
)));
1008
-
let (dir, file_prefix) = prefix
1009
-
.rfind('/')
1010
-
.map(|idx| (&prefix[..idx + 1], &prefix[idx + 1..]))
1011
-
.unwrap_or(("", prefix.as_str()));
1012
-
1013
-
let dir_to_join = (dir.len() > 1 && dir.ends_with('/'))
1014
-
.then(|| &dir[..dir.len() - 1])
1015
-
.unwrap_or(dir);
1016
-
1017
-
let target_dir = if !dir.is_empty() {
1018
-
match root.join(dir_to_join) {
1019
-
Ok(d) if d.is_dir().unwrap_or(false) => Some(d),
1020
-
_ => None,
1021
-
}
1022
-
} else {
1023
-
Some(root.join("").unwrap())
1024
-
};
1025
-
1026
-
let span = to_char_span(span);
1027
-
if let Some(d) = target_dir {
1028
-
if let Ok(iterator) = d.read_dir() {
1029
-
for entry in iterator {
1030
-
let name = entry.filename();
1031
-
if name.starts_with(file_prefix) {
1032
-
let full_completion = format!("{}{}", dir, name);
1033
-
suggestions.push(Suggestion {
1034
-
name: full_completion.clone(),
1035
-
description: None,
1036
-
is_command: false,
1037
-
rendered: full_completion,
1038
-
span_start: span.start,
1039
-
span_end: span.end,
1040
-
});
1041
-
}
1042
-
}
1043
-
}
1044
-
}
1045
-
}
1046
-
}
1047
-
_ => {
1048
-
web_sys::console::log_1(&JsValue::from_str(
1049
-
"[completion] Context is None, no suggestions generated",
1050
-
));
1051
-
}
1052
-
}
1053
-
1054
-
drop(working_set);
1055
-
drop(engine_guard);
1056
-
1057
-
suggestions.sort();
1058
-
let suggestions = serde_json::to_string(&suggestions).unwrap_or_else(|_| "[]".to_string());
1059
-
web_sys::console::log_1(&JsValue::from_str(&suggestions));
1060
-
suggestions
1061
-
}
+1
-1
src/default_context.rs
+1
-1
src/default_context.rs
+71
-17
src/error.rs
+71
-17
src/error.rs
···
1
-
use miette::{GraphicalReportHandler, Report, SourceCode, SourceSpan, SpanContents};
1
+
use miette::{Diagnostic, GraphicalReportHandler, Report, SourceCode, SourceSpan, SpanContents};
2
+
use nu_protocol::{ShellError, Span};
3
+
use vfs::{VfsError, error::VfsErrorKind};
2
4
3
5
pub struct CommandError {
4
6
pub error: Report,
5
7
pub start_offset: usize,
8
+
pub input: String,
9
+
}
10
+
11
+
impl CommandError {
12
+
pub fn new<E>(error: E, input: impl Into<String>) -> Self
13
+
where
14
+
E: Diagnostic + Clone + Send + Sync + 'static,
15
+
{
16
+
Self {
17
+
error: Report::new(error),
18
+
start_offset: 0,
19
+
input: input.into(),
20
+
}
21
+
}
22
+
23
+
pub fn with_start_offset(mut self, start_offset: usize) -> Self {
24
+
self.start_offset = start_offset;
25
+
self
26
+
}
27
+
}
28
+
29
+
impl From<ShellError> for CommandError {
30
+
fn from(value: ShellError) -> Self {
31
+
CommandError::new(value, String::new())
32
+
}
33
+
}
34
+
35
+
impl From<CommandError> for String {
36
+
fn from(value: CommandError) -> Self {
37
+
let handler = GraphicalReportHandler::new()
38
+
.with_theme(miette::GraphicalTheme::unicode())
39
+
.with_cause_chain();
40
+
41
+
if value.input.is_empty() {
42
+
let mut msg = String::new();
43
+
handler
44
+
.render_report(&mut msg, value.error.as_ref())
45
+
.unwrap();
46
+
return msg;
47
+
}
48
+
49
+
let source = OffsetSource {
50
+
source: value.input,
51
+
start_offset: value.start_offset,
52
+
};
53
+
54
+
let report_with_source = value.error.with_source_code(source);
55
+
let mut msg = String::new();
56
+
handler
57
+
.render_report(&mut msg, report_with_source.as_ref())
58
+
.unwrap();
59
+
msg
60
+
}
6
61
}
7
62
8
63
pub struct OffsetSource {
···
59
114
}
60
115
}
61
116
62
-
pub fn format_error(error: Report, input: String, start_offset: usize) -> String {
63
-
let handler = GraphicalReportHandler::new()
64
-
.with_theme(miette::GraphicalTheme::unicode())
65
-
.with_cause_chain();
66
-
67
-
let source = OffsetSource {
68
-
source: input,
69
-
start_offset,
70
-
};
71
-
72
-
let report_with_source = error.with_source_code(source);
73
-
let mut output = String::new();
74
-
handler
75
-
.render_report(&mut output, report_with_source.as_ref())
76
-
.unwrap();
77
-
output
117
+
pub fn to_shell_err(span: Span) -> impl Fn(VfsError) -> ShellError {
118
+
move |vfs_error: VfsError| ShellError::GenericError {
119
+
error: (match vfs_error.kind() {
120
+
VfsErrorKind::DirectoryExists
121
+
| VfsErrorKind::FileExists
122
+
| VfsErrorKind::FileNotFound
123
+
| VfsErrorKind::InvalidPath => "path error",
124
+
_ => "io error",
125
+
})
126
+
.to_string(),
127
+
msg: vfs_error.to_string(),
128
+
span: Some(span),
129
+
help: None,
130
+
inner: vec![],
131
+
}
78
132
}
+34
-53
src/globals.rs
+34
-53
src/globals.rs
···
1
1
use futures::stream::AbortHandle;
2
-
use nu_protocol::{
3
-
ShellError, Signal, Span,
4
-
engine::{EngineState, StateDelta},
5
-
};
2
+
use nu_protocol::Signal;
3
+
use rust_embed::RustEmbed;
6
4
use std::{
7
5
collections::HashMap,
8
6
sync::{
···
11
9
},
12
10
time::{Duration, SystemTime, UNIX_EPOCH},
13
11
};
14
-
use vfs::{VfsError, VfsPath, error::VfsErrorKind};
12
+
use vfs::{EmbeddedFS, OverlayFS, VfsPath};
15
13
use wasm_bindgen::prelude::*;
16
14
17
15
use crate::memory_fs::MemoryFS;
18
16
19
17
static ROOT: OnceLock<Arc<VfsPath>> = OnceLock::new();
20
18
19
+
fn init_vfs() -> Arc<VfsPath> {
20
+
let memory_fs = VfsPath::new(MemoryFS::new());
21
+
let embedded_fs = VfsPath::new(EmbeddedFS::<EmbeddedFiles>::new());
22
+
let overlaid_fs = VfsPath::new(OverlayFS::new(&[memory_fs, embedded_fs]));
23
+
Arc::new(overlaid_fs)
24
+
}
25
+
21
26
pub fn get_vfs() -> Arc<VfsPath> {
22
-
ROOT.get_or_init(|| Arc::new(VfsPath::new(MemoryFS::new())))
23
-
.clone()
27
+
ROOT.get_or_init(init_vfs).clone()
24
28
}
25
29
30
+
#[derive(RustEmbed, Debug)]
31
+
#[folder = "embedded/"]
32
+
#[exclude = ".gitkeep"]
33
+
pub struct EmbeddedFiles;
34
+
26
35
static PWD: OnceLock<RwLock<Arc<VfsPath>>> = OnceLock::new();
27
36
28
37
pub fn get_pwd() -> Arc<VfsPath> {
···
34
43
35
44
pub fn set_pwd(path: Arc<VfsPath>) {
36
45
*PWD.get_or_init(|| RwLock::new(get_vfs())).write().unwrap() = path;
37
-
}
38
-
39
-
pub fn to_shell_err(span: Span) -> impl Fn(VfsError) -> ShellError {
40
-
move |vfs_error: VfsError| ShellError::GenericError {
41
-
error: (match vfs_error.kind() {
42
-
VfsErrorKind::DirectoryExists
43
-
| VfsErrorKind::FileExists
44
-
| VfsErrorKind::FileNotFound
45
-
| VfsErrorKind::InvalidPath => "path error",
46
-
_ => "io error",
47
-
})
48
-
.to_string(),
49
-
msg: vfs_error.to_string(),
50
-
span: Some(span),
51
-
help: None,
52
-
inner: vec![],
53
-
}
54
46
}
55
47
56
48
pub struct TaskInfo {
···
141
133
false
142
134
}
143
135
144
-
static PENDING_DELTAS: OnceLock<Mutex<Vec<StateDelta>>> = OnceLock::new();
136
+
// static PENDING_DELTAS: OnceLock<Mutex<Vec<StateDelta>>> = OnceLock::new();
145
137
146
-
pub fn queue_delta(delta: StateDelta) {
147
-
let _ = PENDING_DELTAS.get_or_init(|| Mutex::new(Vec::new()));
148
-
if let Ok(mut guard) = PENDING_DELTAS.get().unwrap().lock() {
149
-
guard.push(delta);
150
-
}
151
-
}
138
+
// pub fn queue_delta(delta: StateDelta) {
139
+
// let _ = PENDING_DELTAS.get_or_init(|| Mutex::new(Vec::new()));
140
+
// if let Ok(mut guard) = PENDING_DELTAS.get().unwrap().lock() {
141
+
// guard.push(delta);
142
+
// }
143
+
// }
152
144
153
-
pub fn apply_pending_deltas(engine_state: &mut EngineState) -> Result<(), ShellError> {
154
-
if let Some(mutex) = PENDING_DELTAS.get() {
155
-
if let Ok(mut guard) = mutex.lock() {
156
-
for delta in guard.drain(..) {
157
-
engine_state.merge_delta(delta)?;
158
-
}
159
-
}
160
-
}
161
-
Ok(())
162
-
}
145
+
// pub fn apply_pending_deltas(engine_state: &mut EngineState) -> Result<(), ShellError> {
146
+
// if let Some(mutex) = PENDING_DELTAS.get() {
147
+
// if let Ok(mut guard) = mutex.lock() {
148
+
// for delta in guard.drain(..) {
149
+
// engine_state.merge_delta(delta)?;
150
+
// }
151
+
// }
152
+
// }
153
+
// Ok(())
154
+
// }
163
155
164
156
pub static CONSOLE_CALLBACK: OnceLock<Mutex<Option<CallbackWrapper>>> = OnceLock::new();
165
157
···
171
163
}
172
164
}
173
165
174
-
pub fn print_to_console(msg: &str, is_cmd: bool) -> Result<(), ShellError> {
175
-
// if is_interrupted() {
176
-
// return Err(ShellError::Interrupted {
177
-
// span: Span::unknown(),
178
-
// });
179
-
// }
166
+
pub fn print_to_console(msg: &str, is_cmd: bool) {
180
167
if let Some(mutex) = CONSOLE_CALLBACK.get() {
181
168
if let Ok(guard) = mutex.lock() {
182
169
if let Some(cb) = guard.as_ref() {
···
187
174
}
188
175
}
189
176
}
190
-
Ok(())
191
177
}
192
178
193
179
pub fn current_time() -> Option<SystemTime> {
···
203
189
pub static INTERRUPT_BUFFER: RefCell<Option<Int32Array>> = RefCell::new(None);
204
190
}
205
191
206
-
/// Called from JS to pass the SharedArrayBuffer view
207
192
#[wasm_bindgen]
208
193
pub fn set_interrupt_buffer(buffer: Int32Array) {
209
194
INTERRUPT_BUFFER.with(|b| {
···
211
196
});
212
197
}
213
198
214
-
/// Call this function periodically in your long-running loops!
215
-
/// Returns `true` if an interrupt was requested.
216
199
pub fn check_interrupt() -> bool {
217
200
INTERRUPT_BUFFER.with(|b| {
218
201
if let Some(buffer) = b.borrow().as_ref() {
219
-
// Check index 0. If it's 1, an interrupt occurred.
220
-
// We use Atomics to ensure we see the value written by the main thread.
221
202
match js_sys::Atomics::load(buffer, 0) {
222
203
Ok(1) => true,
223
204
_ => false,
+77
-112
src/lib.rs
+77
-112
src/lib.rs
···
1
1
use async_lock::{RwLock, RwLockReadGuard, RwLockUpgradableReadGuard, RwLockWriteGuard};
2
-
use futures::FutureExt;
3
-
use jacquard::chrono;
2
+
use futures::TryFutureExt;
4
3
use js_sys::Promise;
5
-
use miette::Report;
6
4
use nu_cmd_base::hook::eval_hook;
7
5
use nu_cmd_extra::add_extra_command_context;
8
6
use nu_cmd_lang::create_default_context;
9
7
use nu_engine::{command_prelude::*, eval_block};
10
-
use nu_parser::{FlatShape, TokenContents, flatten_block, lex, parse};
8
+
use nu_parser::{FlatShape, flatten_block, parse};
11
9
use nu_protocol::{
12
10
Config, ListStream, PipelineData, Signals, Span,
13
11
engine::{EngineState, Stack, StateWorkingSet},
14
12
};
15
-
use serde::Serialize;
16
13
use std::{
14
+
fmt::Write,
17
15
io::Cursor,
18
16
sync::{Arc, OnceLock},
19
-
time::UNIX_EPOCH,
20
17
};
21
-
use vfs::VfsError;
22
18
use wasm_bindgen::prelude::*;
23
19
use wasm_bindgen_futures::future_to_promise;
24
20
···
32
28
33
29
use crate::{
34
30
cmd::{
35
-
Cd, Fetch, Job, JobKill, JobList, Ls, Mkdir, Open, Pwd, Random, Rm, Save, Source, Sys,
36
-
Version,
31
+
Cd, Eval, Fetch, Glob, Job, JobKill, JobList, Ls, Mkdir, Mv, Open, Print, Pwd, Random, Rm,
32
+
Save, SourceFile, Sys,
37
33
},
38
34
default_context::add_shell_command_context,
39
-
error::format_error,
40
-
globals::{
41
-
InterruptBool, apply_pending_deltas, current_time, get_pwd, print_to_console, set_interrupt,
42
-
},
35
+
globals::{InterruptBool, get_pwd, get_vfs, print_to_console, set_interrupt},
43
36
};
44
37
use error::CommandError;
45
-
use globals::get_vfs;
46
38
47
39
#[wasm_bindgen]
48
40
extern "C" {
···
73
65
static ENGINE_STATE: OnceLock<RwLock<EngineState>> = OnceLock::new();
74
66
#[inline]
75
67
async fn read_engine_state() -> RwLockReadGuard<'static, EngineState> {
76
-
ENGINE_STATE.get().unwrap().read().await
68
+
unsafe { ENGINE_STATE.get().unwrap_unchecked() }
69
+
.read()
70
+
.await
77
71
}
78
72
#[inline]
79
73
async fn write_engine_state() -> RwLockWriteGuard<'static, EngineState> {
80
-
ENGINE_STATE.get().unwrap().write().await
74
+
unsafe { ENGINE_STATE.get().unwrap_unchecked() }
75
+
.write()
76
+
.await
81
77
}
82
78
83
79
static STACK: OnceLock<RwLock<Stack>> = OnceLock::new();
84
80
#[inline]
85
81
async fn read_stack() -> RwLockReadGuard<'static, Stack> {
86
-
STACK.get().unwrap().read().await
82
+
unsafe { STACK.get().unwrap_unchecked() }.read().await
87
83
}
88
84
#[inline]
89
85
async fn write_stack() -> RwLockWriteGuard<'static, Stack> {
90
-
STACK.get().unwrap().write().await
86
+
unsafe { STACK.get().unwrap_unchecked() }.write().await
91
87
}
92
88
93
-
fn init_engine_internal() -> Result<(), Report> {
89
+
#[wasm_bindgen]
90
+
pub fn init_engine() -> Promise {
91
+
std::panic::set_hook(Box::new(panic_hook));
92
+
future_to_promise(
93
+
init_engine_internal()
94
+
.map_ok(|_| JsValue::null())
95
+
.map_err(|s| JsValue::from_str(&s)),
96
+
)
97
+
}
98
+
99
+
async fn init_engine_internal() -> Result<(), String> {
94
100
let mut engine_state = create_default_context();
95
101
engine_state = add_shell_command_context(engine_state);
96
102
engine_state = add_extra_command_context(engine_state);
97
103
98
-
let write_file = |name: &str, contents: &str| {
99
-
get_vfs()
100
-
.join(name)
101
-
.and_then(|p| p.create_file())
102
-
.and_then(|mut f| f.write_all(contents.as_bytes()).map_err(VfsError::from))
103
-
.map_err(|e| miette::miette!(e.to_string()))
104
-
};
105
-
106
-
let access_log = format!(
107
-
r#"/dysnomia.v000 /user: 90008/ /ip: [REDACTED]/ /time: [REDACTED]//
108
-
/dysnomia.v002 /user: 90008/ /ip: [REDACTED]/ /time: [REDACTED]//
109
-
/dysnomia.v011 /user: 90008/ /ip: [REDACTED]/ /time: [REDACTED]//
110
-
[...ENTRIES TRUNCATED...]
111
-
/dysnomia.v099 /user: anonymous/ /ip: [REDACTED]/ /time: {time}//"#,
112
-
time = current_time()
113
-
.and_then(|t| t.duration_since(UNIX_EPOCH).ok())
114
-
.map_or_else(
115
-
|| "unknown".to_string(),
116
-
|time| chrono::DateTime::from_timestamp_nanos(time.as_nanos() as i64)
117
-
.format("%Y-%m-%dT%H:%M:%SZ")
118
-
.to_string()
119
-
)
120
-
);
121
-
write_file(".access.log", &access_log)?;
122
-
123
-
let welcome_txt = r#"welcome anonymous !
124
-
125
-
126
-
you are interfacing with dysnomia.v099
127
-
using the nu shell.
128
-
129
-
130
-
a few commands you can try:
131
-
132
-
"hello, user!" | save message.txt
133
-
fetch at://ptr.pet
134
-
ls --help"#;
135
-
write_file("welcome.txt", &welcome_txt)?;
136
-
137
104
let mut working_set = StateWorkingSet::new(&engine_state);
138
-
let decls: [Box<dyn Command>; 15] = [
105
+
let decls: [Box<dyn Command>; 18] = [
139
106
Box::new(Ls),
140
107
Box::new(Open),
141
108
Box::new(Save),
142
109
Box::new(Mkdir),
110
+
Box::new(Mv),
143
111
Box::new(Pwd),
144
112
Box::new(Cd),
145
113
Box::new(Rm),
146
114
Box::new(Fetch),
147
-
Box::new(Source),
115
+
Box::new(SourceFile),
116
+
Box::new(Eval),
148
117
Box::new(Job),
149
118
Box::new(JobList),
150
119
Box::new(JobKill),
151
120
Box::new(Sys),
152
121
Box::new(Random),
153
-
Box::new(Version),
122
+
Box::new(Print),
123
+
Box::new(Glob),
154
124
];
155
125
for decl in decls {
156
126
working_set.add_decl(decl);
157
127
}
158
-
engine_state.merge_delta(working_set.delta)?;
128
+
engine_state
129
+
.merge_delta(working_set.delta)
130
+
.map_err(CommandError::from)?;
159
131
160
132
let mut config = Config::default();
161
133
config.use_ansi_coloring = true.into();
162
134
config.show_banner = nu_protocol::BannerKind::Full;
163
135
config.hooks.display_output = Some("table".into_value(Span::unknown()));
136
+
config.table.show_empty = false;
164
137
engine_state.config = Arc::new(config);
165
138
166
139
engine_state.set_signals(Signals::new(Arc::new(InterruptBool)));
167
140
168
141
ENGINE_STATE
169
142
.set(RwLock::new(engine_state))
170
-
.map_err(|_| miette::miette!("ENGINE_STATE was already set!?"))?;
143
+
.map_err(|_| "ENGINE_STATE was already set!?".to_string())?;
171
144
STACK
172
145
.set(RwLock::new(Stack::new()))
173
-
.map_err(|_| miette::miette!("STACK was already set!?"))?;
146
+
.map_err(|_| "STACK was already set!?".to_string())?;
174
147
175
-
// web_sys::console::log_1(&"Hello, World!".into());
148
+
let mut startup_script = String::new();
149
+
150
+
// source our "nu rc"
151
+
let rc_path = get_vfs().join("/.env.nu").ok();
152
+
let rc = rc_path.and_then(|env| env.exists().ok().and_then(|ok| ok.then_some(env)));
153
+
if let Some(env) = rc {
154
+
writeln!(&mut startup_script, "eval file {path}", path = env.as_str()).unwrap();
155
+
}
156
+
157
+
// add some aliases for some commands
158
+
let aliases = ["alias l = ls", "alias la = ls -a", "alias . = eval file"];
159
+
for alias in aliases {
160
+
writeln!(&mut startup_script, "{alias}").unwrap();
161
+
}
162
+
163
+
run_command_internal(&startup_script).await?;
176
164
177
165
Ok(())
178
166
}
179
167
180
-
#[wasm_bindgen]
181
-
pub fn init_engine() -> String {
182
-
std::panic::set_hook(Box::new(panic_hook));
183
-
init_engine_internal().map_or_else(|err| format!("error: {err}"), |_| String::new())
184
-
}
185
-
186
-
async fn run_command_internal(input: &str) -> Result<(), CommandError> {
187
-
let mut engine_state = ENGINE_STATE.get().unwrap().upgradable_read().await;
168
+
async fn run_command_internal(input: &str) -> Result<(), String> {
169
+
let mut engine_state = unsafe { ENGINE_STATE.get().unwrap_unchecked() }
170
+
.upgradable_read()
171
+
.await;
188
172
let (mut working_set, signals, config) = {
189
173
let mut write_engine_state = RwLockUpgradableReadGuard::upgrade(engine_state).await;
190
-
apply_pending_deltas(&mut write_engine_state).map_err(|e| CommandError {
191
-
error: Report::new(e),
192
-
start_offset: 0,
193
-
})?;
174
+
// apply_pending_deltas(&mut write_engine_state).map_err(|e| CommandError {
175
+
// error: Report::new(e),
176
+
// start_offset: 0,
177
+
// })?;
194
178
write_engine_state.add_env_var(
195
179
"PWD".to_string(),
196
180
get_pwd_string().into_value(Span::unknown()),
···
206
190
let start_offset = working_set.next_span_start();
207
191
let block = parse(&mut working_set, Some("entry"), input.as_bytes(), false);
208
192
209
-
let cmd_err = |err: ShellError| CommandError {
210
-
error: Report::new(err),
211
-
start_offset,
212
-
};
193
+
let cmd_err = |err: ShellError| CommandError::new(err, input).with_start_offset(start_offset);
213
194
214
195
if let Some(err) = working_set.parse_errors.into_iter().next() {
215
-
return Err(CommandError {
216
-
error: Report::new(err),
217
-
start_offset,
218
-
});
196
+
return Err(CommandError::new(err, input)
197
+
.with_start_offset(start_offset)
198
+
.into());
219
199
}
220
200
if let Some(err) = working_set.compile_errors.into_iter().next() {
221
-
return Err(CommandError {
222
-
error: Report::new(err),
223
-
start_offset,
224
-
});
201
+
return Err(CommandError::new(err, input)
202
+
.with_start_offset(start_offset)
203
+
.into());
225
204
}
226
205
let delta = working_set.delta;
227
206
···
229
208
let mut write_engine_state = RwLockUpgradableReadGuard::upgrade(engine_state).await;
230
209
let mut stack = write_stack().await;
231
210
write_engine_state.merge_delta(delta).map_err(cmd_err)?;
211
+
engine_state = RwLockWriteGuard::downgrade_to_upgradable(write_engine_state);
232
212
let res = eval_block::<nu_protocol::debugger::WithoutDebug>(
233
-
&mut write_engine_state,
213
+
&engine_state,
234
214
&mut stack,
235
215
&block,
236
216
PipelineData::Empty,
237
217
);
238
-
engine_state = RwLockWriteGuard::downgrade_to_upgradable(write_engine_state);
218
+
// apply_pending_deltas(&mut write_engine_state).map_err(cmd_err)?;
239
219
res
240
220
};
241
221
···
249
229
let pipeline_data = match pipeline_data {
250
230
PipelineData::Empty => return Ok(()),
251
231
PipelineData::Value(Value::Error { error, .. }, _) => {
252
-
return Err(cmd_err(*error));
232
+
return Err(cmd_err(*error).into());
253
233
}
254
234
PipelineData::ByteStream(s, m) => match (s.span(), s.type_(), s.reader()) {
255
235
(span, ty, Some(r)) => {
···
297
277
match res {
298
278
PipelineData::Empty => {}
299
279
PipelineData::Value(v, _) => {
300
-
print_to_console(&v.to_expanded_string("\n", &config), true).map_err(cmd_err)?;
280
+
print_to_console(&v.to_expanded_string("\n", &config), true);
301
281
}
302
282
PipelineData::ByteStream(s, _) => {
303
283
for line in s.lines().into_iter().flatten() {
304
284
let out = line.map_err(cmd_err)?; // TODO: do we turn this into a Value ??? or is returning err fine
305
-
print_to_console(&out, true).map_err(cmd_err)?;
285
+
print_to_console(&out, true);
306
286
}
307
287
}
308
288
PipelineData::ListStream(s, _) => {
···
311
291
.unwrap_error()
312
292
.map_err(cmd_err)?
313
293
.to_expanded_string("\n", &config);
314
-
print_to_console(&out, true).map_err(cmd_err)?;
294
+
print_to_console(&out, true);
315
295
}
316
296
}
317
297
}
···
325
305
326
306
future_to_promise(async move {
327
307
run_command_internal(&input)
328
-
.map(|res| {
329
-
res.map_or_else(
330
-
|cmd_err| {
331
-
Some(format_error(
332
-
cmd_err.error,
333
-
input.to_owned(),
334
-
cmd_err.start_offset,
335
-
))
336
-
},
337
-
|_| None,
338
-
)
339
-
})
340
-
.map(|res| {
341
-
Ok(res
342
-
.map(|s| JsValue::from_str(&s))
343
-
.unwrap_or_else(JsValue::null))
344
-
})
308
+
.map_ok(|_| JsValue::null())
309
+
.map_err(|s| JsValue::from_str(&s))
345
310
.await
346
311
})
347
312
}
+10
-10
www/index.html
+10
-10
www/index.html
···
1
1
<!doctype html>
2
2
<html lang="en">
3
-
<head>
4
-
<meta charset="UTF-8" />
5
-
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
-
<title>dysnomia</title>
8
-
</head>
9
-
<body>
10
-
<div id="app"></div>
11
-
<script type="module" src="/src/main.ts"></script>
12
-
</body>
3
+
<head>
4
+
<meta charset="UTF-8" />
5
+
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+
<title>dysnomia</title>
8
+
</head>
9
+
<body>
10
+
<div id="app"></div>
11
+
<script type="module" src="/src/main.ts"></script>
12
+
</body>
13
13
</html>
+1
-1
www/package.json
+1
-1
www/package.json
www/public/Web437_EverexME_5x8.woff
www/public/Web437_EverexME_5x8.woff
This is a binary file and will not be displayed.
+44
-33
www/src/main.ts
+44
-33
www/src/main.ts
···
14
14
type Candidate = {
15
15
name: string;
16
16
description: string;
17
-
is_command: boolean;
18
17
rendered: string;
19
18
span_start: number;
20
19
span_end: number;
···
47
46
});
48
47
};
49
48
49
+
await document.fonts.load("16px EverexME");
50
+
50
51
const term = new Terminal({
51
52
cursorBlink: true,
52
-
cursorStyle: "bar",
53
+
cursorStyle: "block",
53
54
cursorInactiveStyle: "block",
54
-
fontFamily: '"Comic Mono", monospace',
55
-
fontSize: 15,
55
+
customGlyphs: true,
56
+
fontFamily: "EverexME",
57
+
fontSize: 16,
58
+
lineHeight: 1,
59
+
letterSpacing: 0,
56
60
scrollback: 100000,
57
61
theme: {
58
62
background: "#000000",
···
247
251
cursorPos = (before + candidate.name).length;
248
252
};
249
253
254
+
const applyCompletionFromBase = (baseLine: string, candidate: Candidate) => {
255
+
currentLine = getLineWithCompletion(baseLine, candidate);
256
+
const before = baseLine.slice(0, candidate.span_start);
257
+
cursorPos = (before + candidate.name).length;
258
+
};
259
+
250
260
const updateCompletionCandidates = async () => {
251
261
// Don't try to get completion if worker isn't loaded
252
262
if (!isWorkerReady) return;
···
294
304
completionCandidates = matches.map((cmd) => ({
295
305
name: cmd,
296
306
description: "",
297
-
is_command: true,
298
307
rendered: cmd,
299
308
span_start: 0,
300
309
span_end: currentLine.length,
···
309
318
}
310
319
};
311
320
312
-
// 1. Setup Input Handler immediately so terminal isn't blocked
313
321
term.onData(async (e) => {
314
322
// SEARCH MODE INPUT
315
323
if (searchState !== "none" && e >= " " && e <= "~") {
···
370
378
if (completionCandidates.length > 0) {
371
379
const cand = completionCandidates[completionIndex];
372
380
if (cand) {
373
-
currentLine = getLineWithCompletion(completionBaseLine, cand);
374
-
cursorPos = currentLine.length;
381
+
applyCompletionFromBase(completionBaseLine, cand);
375
382
}
376
383
}
377
384
···
401
408
"\x1b[33mengine is still loading, please wait (_ _ )zZ...\x1b[0m\r\n",
402
409
);
403
410
} else {
404
-
// ASYNC execution
405
-
const output: string | undefined = await callWorker(
406
-
"run",
407
-
trimmed,
408
-
);
409
-
if (output) {
410
-
term.write(output.replace(/\n/g, "\r\n"));
411
-
if (output && !output.endsWith("\n")) {
412
-
term.write("\r\n");
411
+
try {
412
+
const output: string | undefined = await callWorker(
413
+
"run",
414
+
trimmed,
415
+
);
416
+
if (output) {
417
+
term.write(output.replace(/\n/g, "\r\n"));
418
+
if (output && !output.endsWith("\n")) {
419
+
term.write("\r\n");
420
+
}
413
421
}
422
+
423
+
// update history
424
+
const idx = history.indexOf(trimmed);
425
+
if (idx >= 0) history.splice(idx, 1);
426
+
history.push(trimmed);
427
+
historyIndex = history.length;
428
+
} catch (error) {
429
+
term.write(`${error}`.replace(/\n/g, "\r\n"));
414
430
}
415
431
416
-
// Update cached PWD after command execution (cd, etc)
432
+
// update pwd
417
433
cachedPwd = await callWorker("get_pwd");
418
-
419
-
// Update History
420
-
const idx = history.indexOf(trimmed);
421
-
if (idx >= 0) history.splice(idx, 1);
422
-
history.push(trimmed);
423
-
historyIndex = history.length;
424
434
}
425
435
} catch (err) {
426
-
term.write(`\x1b[31mfatal: ${err}\x1b[0m\r\n`);
436
+
term.write(
437
+
`\x1b[31mfatal: ${err}\x1b[0m\r\n`.replace(/\n/g, "\r\n"),
438
+
);
427
439
} finally {
428
440
isRunningCommand = false;
429
441
}
···
516
528
(completionIndex - 1 + completionCandidates.length) %
517
529
completionCandidates.length;
518
530
const candidate = completionCandidates[completionIndex];
519
-
currentLine = getLineWithCompletion(completionBaseLine, candidate);
520
-
cursorPos = currentLine.length;
531
+
applyCompletionFromBase(completionBaseLine, candidate);
521
532
await refreshPrompt();
522
533
break;
523
534
}
···
547
558
if (completionCandidates.length > 0 && e === "\x1b[B") {
548
559
completionIndex = (completionIndex + 1) % completionCandidates.length;
549
560
const candidate = completionCandidates[completionIndex];
550
-
currentLine = getLineWithCompletion(completionBaseLine, candidate);
551
-
cursorPos = currentLine.length;
561
+
applyCompletionFromBase(completionBaseLine, candidate);
552
562
await refreshPrompt();
553
563
break;
554
564
}
···
583
593
(completionIndex + 1) % completionCandidates.length;
584
594
585
595
const candidate = completionCandidates[completionIndex];
586
-
currentLine = getLineWithCompletion(completionBaseLine, candidate);
596
+
applyCompletionFromBase(completionBaseLine, candidate);
587
597
await refreshPrompt();
588
598
} else {
589
599
// Guard: ensure worker is ready
···
618
628
(completionIndex - 1 + completionCandidates.length) %
619
629
completionCandidates.length;
620
630
const candidate = completionCandidates[completionIndex];
621
-
currentLine = getLineWithCompletion(completionBaseLine, candidate);
622
-
cursorPos = currentLine.length;
631
+
applyCompletionFromBase(completionBaseLine, candidate);
623
632
await refreshPrompt();
624
633
}
625
634
break;
···
647
656
await readyPromise;
648
657
649
658
await callWorker("set-interrupt-buffer", interruptBuffer);
650
-
await callWorker("run", "open welcome.txt");
651
659
652
660
term.write(getPrompt());
653
661
···
660
668
.catch((e) => {
661
669
term.write(`\r\n\x1b[31mfatal: failed to load engine: ${e}\x1b[0m\r\n`);
662
670
});
671
+
672
+
fitAddon.fit();
673
+
// setTimeout(() => fitAddon.fit(), 1000);
663
674
}
664
675
665
676
bootstrap().catch(console.error);
+196
-2
www/src/style.css
+196
-2
www/src/style.css
···
1
+
@font-face {
2
+
font-family: "EverexME";
3
+
src: url("/Web437_EverexME_5x8.woff") format("woff");
4
+
font-weight: normal;
5
+
font-style: normal;
6
+
}
7
+
8
+
:root {
9
+
--screen-background: #121010;
10
+
11
+
--flicker-opacity: 0.3;
12
+
--flicker-speed: 0.1s;
13
+
14
+
--scanline-opacity: 0.25;
15
+
--scanline-size: 2px;
16
+
17
+
--rgb-red-opacity: 0.12;
18
+
--rgb-green-opacity: 0.08;
19
+
--rgb-blue-opacity: 0.18;
20
+
--rgb-stripe-size: 3px;
21
+
22
+
--crt-contrast: 1;
23
+
--crt-brightness: 1.2;
24
+
--crt-saturation: 1.3;
25
+
}
26
+
1
27
body {
2
28
margin: 0;
3
29
padding: 0;
4
-
background-color: #000000;
30
+
background-color: var(--screen-background);
5
31
height: 100vh;
6
32
width: 100vw;
7
33
overflow: hidden;
8
34
}
9
35
36
+
@keyframes flicker {
37
+
0% {
38
+
opacity: 0.27861;
39
+
}
40
+
3% {
41
+
opacity: 0.34769;
42
+
}
43
+
6% {
44
+
opacity: 0.23604;
45
+
}
46
+
7% {
47
+
opacity: 0.90626;
48
+
}
49
+
8% {
50
+
opacity: 0.18128;
51
+
}
52
+
11% {
53
+
opacity: 0.83891;
54
+
}
55
+
19% {
56
+
opacity: 0.65583;
57
+
}
58
+
20% {
59
+
opacity: 0.67807;
60
+
}
61
+
24% {
62
+
opacity: 0.26559;
63
+
}
64
+
27% {
65
+
opacity: 0.84693;
66
+
}
67
+
40% {
68
+
opacity: 0.96019;
69
+
}
70
+
41% {
71
+
opacity: 0.08594;
72
+
}
73
+
43% {
74
+
opacity: 0.20313;
75
+
}
76
+
53% {
77
+
opacity: 0.71988;
78
+
}
79
+
57% {
80
+
opacity: 0.53455;
81
+
}
82
+
64% {
83
+
opacity: 0.37288;
84
+
}
85
+
68% {
86
+
opacity: 0.71428;
87
+
}
88
+
76% {
89
+
opacity: 0.70419;
90
+
}
91
+
81% {
92
+
opacity: 0.7003;
93
+
}
94
+
86% {
95
+
opacity: 0.36108;
96
+
}
97
+
95% {
98
+
opacity: 0.24387;
99
+
}
100
+
100% {
101
+
opacity: 0.27861;
102
+
}
103
+
}
104
+
105
+
@keyframes flicker-speed {
106
+
0% {
107
+
animation-duration: 0.12s;
108
+
}
109
+
10% {
110
+
animation-duration: 0.08s;
111
+
}
112
+
20% {
113
+
animation-duration: 0.15s;
114
+
}
115
+
30% {
116
+
animation-duration: 0.09s;
117
+
}
118
+
40% {
119
+
animation-duration: 0.13s;
120
+
}
121
+
50% {
122
+
animation-duration: 0.11s;
123
+
}
124
+
60% {
125
+
animation-duration: 0.14s;
126
+
}
127
+
70% {
128
+
animation-duration: 0.07s;
129
+
}
130
+
80% {
131
+
animation-duration: 0.16s;
132
+
}
133
+
90% {
134
+
animation-duration: 0.1s;
135
+
}
136
+
100% {
137
+
animation-duration: 0.12s;
138
+
}
139
+
}
140
+
10
141
#terminal {
142
+
font-family: "EverexME", monospace;
143
+
font-size: 16px;
144
+
145
+
image-rendering: pixelated;
146
+
-webkit-font-smoothing: none;
147
+
-moz-osx-font-smoothing: grayscale;
148
+
font-smooth: never;
149
+
150
+
position: relative;
151
+
overflow: hidden;
152
+
filter: contrast(var(--crt-contrast)) brightness(var(--crt-brightness))
153
+
saturate(var(--crt-saturation));
154
+
155
+
width: 100vw;
11
156
height: 100vh;
12
-
width: 100vw;
157
+
}
158
+
159
+
#terminal::after {
160
+
content: " ";
161
+
display: block;
162
+
position: absolute;
163
+
top: 0;
164
+
left: 0;
165
+
bottom: 0;
166
+
right: 0;
167
+
background: rgba(18, 16, 16, var(--flicker-opacity));
168
+
opacity: 0;
169
+
z-index: 2;
170
+
pointer-events: none;
171
+
animation:
172
+
flicker var(--flicker-speed) steps(2, jump-none) infinite,
173
+
flicker-speed 3.7s steps(10, jump-none) infinite;
174
+
}
175
+
176
+
@media (prefers-reduced-motion: reduce) {
177
+
#terminal::after {
178
+
animation: none;
179
+
opacity: 0.1;
180
+
}
181
+
}
182
+
183
+
#terminal::before {
184
+
content: " ";
185
+
display: block;
186
+
position: absolute;
187
+
top: 0;
188
+
left: 0;
189
+
bottom: 0;
190
+
right: 0;
191
+
background:
192
+
linear-gradient(
193
+
rgba(18, 16, 16, 0) 50%,
194
+
rgba(0, 0, 0, var(--scanline-opacity)) 50%
195
+
),
196
+
linear-gradient(
197
+
90deg,
198
+
rgba(255, 0, 0, var(--rgb-red-opacity)),
199
+
rgba(0, 255, 0, var(--rgb-green-opacity)),
200
+
rgba(0, 0, 255, var(--rgb-blue-opacity))
201
+
);
202
+
z-index: 2;
203
+
background-size:
204
+
100% var(--scanline-size),
205
+
var(--rgb-stripe-size) 100%;
206
+
pointer-events: none;
13
207
}
+7
-4
www/src/worker.ts
+7
-4
www/src/worker.ts
···
11
11
12
12
// Initialize WASM
13
13
await init();
14
-
init_engine();
15
14
16
-
// Setup Callbacks to proxy messages back to Main Thread
17
15
register_console_callback((msg: string, isCmd: boolean) => {
18
16
self.postMessage({ type: "console", payload: { msg, isCmd } });
19
17
});
···
22
20
self.postMessage({ type: "task_count", payload: count });
23
21
});
24
22
25
-
// Handle messages from Main Thread
23
+
try {
24
+
await init_engine();
25
+
} catch (error) {
26
+
console.error(error);
27
+
}
28
+
26
29
self.onmessage = async (e) => {
27
30
const { id, type, payload } = e.data;
28
31
···
47
50
result = get_pwd_string();
48
51
break;
49
52
default:
50
-
throw new Error(`Unknown message type: ${type}`);
53
+
throw new Error(`unknown message type: ${type}`);
51
54
}
52
55
self.postMessage({ id, type: `${type}_result`, payload: result });
53
56
} catch (err) {