Compare changes

Choose any two refs to compare.

+2 -1
.gitignore
··· 13 13 # Env 14 14 .env 15 15 .env.* 16 + !.env.nu 16 17 !.env.example 17 18 !.env.test 18 19 ··· 26 27 target 27 28 28 29 # ai scratchpad 29 - /.scratchpad 30 + /.scratchpad
+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
··· 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
··· 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
··· 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
··· 1 + def "dys version" [] { 2 + let time = sys | get build_time 3 + print $"dysnomia.v099.t($time)" 4 + }
+16
embedded/.dys/routine/welcome.nu
··· 1 + def "dys welcome" [] { 2 + print ' 3 + welcome anonymous ! 4 + 5 + 6 + you are interfacing with dysnomia.v099 7 + using the nu shell. 8 + 9 + 10 + a few commands you can try: 11 + 12 + "hello, user!" | save message.txt 13 + fetch at://ptr.pet 14 + ls --help 15 + ' 16 + }
+5
embedded/.dys/startup.nu
··· 1 + $env.LOGIN_TIME = date now | format date "%s" 2 + 3 + eval file routine/*.nu 4 + 5 + eval "dys welcome"
+1
embedded/.env.nu
··· 1 + . .dys/startup.nu
embedded/.gitkeep

This is a binary file and will not be displayed.

+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"
+4 -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="; 32 33 "nu-cmd-base-0.109.2" = "sha256-Q+6PxSmeiV/K6QP0I9xCiqZM37+p+CRLs7LMBUWurPo="; 33 34 }; 34 35 }; 35 36 36 - nativeBuildInputs = [wasm-pack wasm-bindgen-cli lld]; 37 + nativeBuildInputs = [wasm-pack wasm-bindgen-cli_0_2_104 lld]; 37 38 38 39 phases = ["unpackPhase" "buildPhase"]; 39 40
+5 -2
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 7 Category, IntoValue, PipelineData, ShellError, Signature, SyntaxShape, Type, ··· 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") 21 24 .input_output_type(Type::Nothing, Type::Nothing) 22 25 .category(Category::FileSystem) 23 26 }
+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 + }
+2 -1
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;
+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 + }
+1 -1
src/cmd/job_list.rs
··· 14 14 15 15 fn signature(&self) -> Signature { 16 16 Signature::build("job list") 17 - .input_output_type(Type::Nothing, Type::record()) 17 + .input_output_type(Type::Nothing, Type::table()) 18 18 .category(Category::System) 19 19 } 20 20
+104 -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 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')) 37 - .input_output_type(Type::Nothing, Type::record()) 42 + .input_output_type(Type::Nothing, Type::table()) 38 43 .category(Category::FileSystem) 39 44 } 40 45 ··· 49 54 call: &nu_protocol::engine::Call, 50 55 _input: PipelineData, 51 56 ) -> Result<PipelineData, ShellError> { 52 - let path_arg: Option<String> = call.opt(engine_state, stack, 0)?; 57 + let path_arg: Option<Value> = call.opt(engine_state, stack, 0)?; 53 58 let all = call.has_flag(engine_state, stack, "all")?; 54 59 let long = call.has_flag(engine_state, stack, "long")?; 55 60 let full_paths = call.has_flag(engine_state, stack, "full-paths")?; 56 61 57 62 let pwd = get_pwd(); 58 - let mut target_dir = pwd.clone(); 59 - if let Some(path) = path_arg { 60 - target_dir = Arc::new( 61 - target_dir 62 - .join(path.trim_end_matches('/')) 63 - .map_err(to_shell_err(call.arguments_span()))?, 64 - ); 65 - } 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("**"); 88 + 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()))?; 104 + 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 + }; 66 135 67 - let span = call.head; 68 - let entries = target_dir.read_dir().map_err(to_shell_err(span))?; 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))?; 69 139 70 - let make_record = move |name: &str, metadata: &vfs::VfsMetadata| { 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 + 71 146 let type_str = match metadata.file_type { 72 147 vfs::VfsFileType::Directory => "dir", 73 148 vfs::VfsFileType::File => "file", 74 149 }; 75 150 76 151 let mut record = Record::new(); 77 - 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)); 78 158 record.push("type", Value::string(type_str, span)); 79 159 record.push("size", Value::filesize(metadata.len as i64, span)); 80 160 let mut add_timestamp = |field: &str, timestamp: Option<SystemTime>| { ··· 96 176 add_timestamp("created", metadata.created); 97 177 add_timestamp("accessed", metadata.accessed); 98 178 } 99 - Value::record(record, span) 179 + Ok(Some(Value::record(record, span))) 100 180 }; 101 181 102 - let entries = entries.into_iter().flat_map(move |entry| { 103 - let do_map = || { 104 - let name = entry.filename(); 105 - if name.starts_with('.') && !all { 106 - return Ok(None); 107 - } 108 - let metadata = entry.metadata().map_err(to_shell_err(span))?; 109 - 110 - let name = if full_paths { 111 - format!("{path}/{name}", path = target_dir.as_str()) 112 - } else { 113 - let path = target_dir 114 - .as_str() 115 - .trim_start_matches(pwd.as_str()) 116 - .trim_start_matches("/"); 117 - format!( 118 - "{path}{sep}{name}", 119 - sep = path.is_empty().then_some("").unwrap_or("/"), 120 - ) 121 - }; 122 - Ok(Some(make_record(&name, &metadata))) 123 - }; 124 - do_map() 182 + let entries = matches.into_iter().flat_map(move |rel_path| { 183 + make_record(&rel_path) 125 184 .transpose() 126 185 .map(|res| res.unwrap_or_else(|err| Value::error(err, span))) 127 186 });
+2 -2
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 4 Category, PipelineData, ShellError, Signature, SyntaxShape, Type, ··· 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 23 .input_output_type(Type::Nothing, Type::Nothing)
+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
··· 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 + }
+127 -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, Type, 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", ··· 39 48 call: &nu_protocol::engine::Call, 40 49 _input: PipelineData, 41 50 ) -> Result<PipelineData, ShellError> { 42 - let path: String = call.req(engine_state, stack, 0)?; 51 + let path_value: Value = call.req(engine_state, stack, 0)?; 43 52 let raw_flag = call.has_flag(engine_state, stack, "raw")?; 44 53 45 - 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 + }; 76 + 77 + let matches = expand_path(&path_str, base_path.clone(), options)?; 78 + 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; 99 + } 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(); 46 110 47 - let parse_cmd = raw_flag 48 - .not() 49 - .then(|| { 50 - target_file 51 - .extension() 52 - .and_then(|ext| get_cmd_for_ext(&ext)) 53 - }) 54 - .flatten(); 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 + ); 55 122 56 - target_file 57 - .open_file() 58 - .map_err(to_shell_err(call.head)) 59 - .and_then(|f| { 60 - let data = PipelineData::ByteStream( 61 - ByteStream::read( 62 - f, 63 - call.head, 64 - engine_state.signals().clone(), 65 - nu_protocol::ByteStreamType::String, 66 - ), 67 - None, 68 - ); 69 - if let Some(cmd) = parse_cmd { 70 - return cmd.run(engine_state, stack, call, data); 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); 71 138 } 72 - Ok(data) 73 - }) 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 + } 74 171 } 75 172 } 76 173
+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 + }
+52 -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, Type, 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( ··· 41 46 call: &nu_protocol::engine::Call, 42 47 _input: PipelineData, 43 48 ) -> Result<PipelineData, ShellError> { 44 - let path: String = call.req(engine_state, stack, 0)?; 49 + let path_value: Value = call.req(engine_state, stack, 0)?; 45 50 let recursive = call.has_flag(engine_state, stack, "recursive")?; 46 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 + 47 65 // Prevent removing root 48 - if path == "/" { 66 + if path_str == "/" { 49 67 return Err(ShellError::GenericError { 50 68 error: "cannot remove root".to_string(), 51 69 msg: "refusing to remove root directory".to_string(), ··· 55 73 }); 56 74 } 57 75 58 - // Resolve target relative to PWD (or absolute if path starts with '/') 59 - let target = get_pwd() 60 - .join(path.trim_end_matches('/')) 61 - .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() }; 62 79 63 - let meta = target.metadata().map_err(to_shell_err(call.head))?; 64 - match meta.file_type { 65 - VfsFileType::File => { 66 - target.remove_file().map_err(to_shell_err(call.head))?; 67 - Ok(PipelineData::Empty) 68 - } 69 - VfsFileType::Directory => { 70 - (if recursive { 71 - target.remove_dir_all() 72 - } else { 73 - // non-recursive: attempt to remove directory (will fail if not empty) 74 - target.remove_dir() 75 - }) 76 - .map_err(to_shell_err(call.head)) 77 - .map(|_| PipelineData::Empty) 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)?; 87 + 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 + } 78 104 } 79 105 } 106 + 107 + Ok(PipelineData::Empty) 80 108 } 81 109 }
+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 }
-90
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, Type, 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 - .input_output_type(Type::Nothing, Type::Nothing) 23 - .category(Category::Core) 24 - } 25 - 26 - fn description(&self) -> &str { 27 - "source a file from the virtual filesystem." 28 - } 29 - 30 - fn run( 31 - &self, 32 - engine_state: &EngineState, 33 - stack: &mut Stack, 34 - call: &nu_protocol::engine::Call, 35 - input: PipelineData, 36 - ) -> Result<PipelineData, ShellError> { 37 - let filename: String = call.req(engine_state, stack, 0)?; 38 - 39 - // 1. Read file from VFS 40 - let path = get_pwd().join(&filename).map_err(to_shell_err(call.head))?; 41 - let mut file = path.open_file().map_err(to_shell_err(call.head))?; 42 - let mut contents = String::new(); 43 - file.read_to_string(&mut contents).map_err(|e| { 44 - ShellError::Io(IoError::new( 45 - e, 46 - call.head, 47 - PathBuf::from_str(path.as_str()).unwrap(), 48 - )) 49 - })?; 50 - 51 - // 2. Parse the content 52 - // We create a new working set based on the CURRENT engine state. 53 - let mut working_set = StateWorkingSet::new(engine_state); 54 - 55 - // We must add the file to the working set so the parser can track spans correctly 56 - let _file_id = working_set.add_file(filename.clone(), contents.as_bytes()); 57 - 58 - // Parse the block 59 - let block = parse( 60 - &mut working_set, 61 - Some(&filename), 62 - contents.as_bytes(), 63 - false, 64 - ); 65 - 66 - if let Some(err) = working_set.parse_errors.first() { 67 - return Err(ShellError::GenericError { 68 - error: "Parse error".into(), 69 - msg: err.to_string(), 70 - span: Some(call.head), 71 - help: None, 72 - inner: vec![], 73 - }); 74 - } 75 - 76 - // 3. Prepare execution context 77 - // We clone the engine state to merge the new definitions (delta) locally. 78 - // This ensures the script can call its own defined functions immediately. 79 - let mut local_state = engine_state.clone(); 80 - local_state.merge_delta(working_set.delta.clone())?; 81 - 82 - // 4. Queue the delta for the global engine state 83 - // This allows definitions to be available in the next command execution cycle (REPL behavior). 84 - queue_delta(working_set.delta); 85 - 86 - // 5. Evaluate the block 87 - // We pass the MUTABLE stack, so environment variable changes (PWD, load-env) WILL persist. 88 - eval_block::<WithoutDebug>(&local_state, stack, &block, input).map(|data| data.body) 89 - } 90 - }
+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 + }
+6
src/cmd/sys.rs
··· 180 180 ); 181 181 } 182 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)); 188 + 183 189 Ok(Value::record(rec, head).into_pipeline_data()) 184 190 } 185 191 }
-35
src/cmd/version.rs
··· 1 - use nu_protocol::Type; 2 - use nu_protocol::engine::Call; 3 - use nu_protocol::{ 4 - Category, IntoPipelineData, PipelineData, ShellError, Signature, Value, 5 - engine::{Command, EngineState, Stack}, 6 - }; 7 - 8 - #[derive(Clone)] 9 - pub struct Version; 10 - 11 - impl Command for Version { 12 - fn name(&self) -> &str { 13 - "version" 14 - } 15 - 16 - fn signature(&self) -> Signature { 17 - Signature::build(self.name()) 18 - .input_output_type(Type::Nothing, Type::String) 19 - .category(Category::System) 20 - } 21 - 22 - fn description(&self) -> &str { 23 - "print the version of dysnomia." 24 - } 25 - 26 - fn run( 27 - &self, 28 - _engine_state: &EngineState, 29 - _stack: &mut Stack, 30 - call: &Call, 31 - _input: PipelineData, 32 - ) -> Result<PipelineData, ShellError> { 33 - Ok(Value::string("dysnomia.v099.t1765660500", call.head).into_pipeline_data()) 34 - } 35 - }
+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
··· 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
··· 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
··· 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(&current_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
··· 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
··· 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 &current { 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 + }
-1984
src/completion.rs
··· 1 - use futures::FutureExt; 2 - use js_sys::Promise; 3 - use nu_protocol::{ENV_VARIABLE_ID, IN_VARIABLE_ID, NU_VARIABLE_ID, Value}; 4 - use std::collections::HashMap; 5 - use wasm_bindgen_futures::future_to_promise; 6 - 7 - use super::*; 8 - 9 - #[derive(Debug, Serialize)] 10 - struct Suggestion { 11 - name: String, 12 - description: Option<String>, 13 - is_command: bool, 14 - rendered: String, 15 - span_start: usize, // char index (not byte) 16 - span_end: usize, // char index (not byte) 17 - } 18 - 19 - impl PartialEq for Suggestion { 20 - fn eq(&self, other: &Self) -> bool { 21 - self.name == other.name 22 - } 23 - } 24 - impl Eq for Suggestion {} 25 - impl PartialOrd for Suggestion { 26 - fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> { 27 - self.name.partial_cmp(&other.name) 28 - } 29 - } 30 - impl Ord for Suggestion { 31 - fn cmp(&self, other: &Self) -> std::cmp::Ordering { 32 - self.name.cmp(&other.name) 33 - } 34 - } 35 - 36 - #[wasm_bindgen] 37 - pub fn completion(input: String, js_cursor_pos: usize) -> Promise { 38 - future_to_promise(completion_impl(input, js_cursor_pos).map(|s| Ok(JsValue::from_str(&s)))) 39 - } 40 - 41 - pub async fn completion_impl(input: String, js_cursor_pos: usize) -> String { 42 - let engine_guard = read_engine_state().await; 43 - let stack_guard = crate::read_stack().await; 44 - let root = get_pwd(); 45 - 46 - // Map UTF-16 cursor position (from JS) to Byte index (for Rust) 47 - let byte_pos = input 48 - .char_indices() 49 - .map(|(i, _)| i) 50 - .nth(js_cursor_pos) 51 - .unwrap_or(input.len()); 52 - 53 - let (working_set, shapes, global_offset) = { 54 - let mut working_set = StateWorkingSet::new(&engine_guard); 55 - let global_offset = working_set.next_span_start(); 56 - let block = parse(&mut working_set, None, input.as_bytes(), false); 57 - let shapes = flatten_block(&working_set, &block); 58 - (working_set, shapes, global_offset) 59 - }; 60 - 61 - // Initial state logging 62 - web_sys::console::log_1(&JsValue::from_str(&format!( 63 - "[completion] Input: {:?}, JS cursor: {}, byte cursor: {}", 64 - input, js_cursor_pos, byte_pos 65 - ))); 66 - web_sys::console::log_1(&JsValue::from_str(&format!( 67 - "[completion] Found {} shapes, global_offset: {}", 68 - shapes.len(), 69 - global_offset 70 - ))); 71 - for (idx, (span, shape)) in shapes.iter().enumerate() { 72 - let (local_start, local_end) = ( 73 - span.start.saturating_sub(global_offset), 74 - span.end.saturating_sub(global_offset), 75 - ); 76 - web_sys::console::log_1(&JsValue::from_str(&format!( 77 - "[completion] Shape {}: {:?} at [{}, {}] (local: [{}, {}])", 78 - idx, shape, span.start, span.end, local_start, local_end 79 - ))); 80 - } 81 - 82 - // Helper functions 83 - let is_separator_char = |c: char| -> bool { ['|', ';', '(', '{'].contains(&c) }; 84 - 85 - let is_command_separator_char = |c: char| -> bool { ['|', ';'].contains(&c) }; 86 - 87 - let has_separator_between = |start: usize, end: usize| -> bool { 88 - if start < end && start < input.len() { 89 - let text_between = &input[start..std::cmp::min(end, input.len())]; 90 - text_between.chars().any(|c| is_separator_char(c)) 91 - } else { 92 - false 93 - } 94 - }; 95 - 96 - let find_last_separator_pos = |text: &str| -> Option<usize> { 97 - text.rfind(|c| is_command_separator_char(c)).map(|i| i + 1) 98 - }; 99 - 100 - let ends_with_separator = |text: &str| -> bool { 101 - let text = text.trim_end(); 102 - text.ends_with('|') || text.ends_with(';') 103 - }; 104 - 105 - let to_local_span = |span: Span| -> Span { 106 - Span::new( 107 - span.start.saturating_sub(global_offset), 108 - span.end.saturating_sub(global_offset), 109 - ) 110 - }; 111 - 112 - let safe_slice = |span: Span| -> String { 113 - (span.start < input.len()) 114 - .then(|| { 115 - let safe_end = std::cmp::min(span.end, input.len()); 116 - input[span.start..safe_end].to_string() 117 - }) 118 - .unwrap_or_default() 119 - }; 120 - 121 - let is_command_shape = |shape: &FlatShape, local_span: Span| -> bool { 122 - matches!( 123 - shape, 124 - FlatShape::External(_) | FlatShape::InternalCall(_) | FlatShape::Keyword 125 - ) || matches!(shape, FlatShape::Garbage) && { 126 - if local_span.start < input.len() { 127 - let prev_text = &safe_slice(local_span); 128 - !prev_text.trim().starts_with('-') 129 - } else { 130 - false 131 - } 132 - } 133 - }; 134 - 135 - let handle_block_prefix = |prefix: &str, span: Span| -> Option<(String, Span, bool)> { 136 - let mut block_prefix = prefix; 137 - let mut block_span_start = span.start; 138 - 139 - // Remove leading '{' and whitespace 140 - if block_prefix.starts_with('{') { 141 - block_prefix = &block_prefix[1..]; 142 - block_span_start += 1; 143 - } 144 - let trimmed_block_prefix = block_prefix.trim_start(); 145 - if trimmed_block_prefix != block_prefix { 146 - // Adjust span start to skip whitespace 147 - block_span_start += block_prefix.len() - trimmed_block_prefix.len(); 148 - } 149 - 150 - let is_empty = trimmed_block_prefix.is_empty(); 151 - Some(( 152 - trimmed_block_prefix.to_string(), 153 - Span::new(block_span_start, span.end), 154 - is_empty, 155 - )) 156 - }; 157 - 158 - // Helper function to find command name and count arguments before cursor 159 - let find_command_and_arg_index = 160 - |current_idx: usize, current_local_span: Span| -> Option<(String, usize)> { 161 - let mut command_name: Option<String> = None; 162 - let mut arg_count = 0; 163 - 164 - // Look backwards through shapes to find the command 165 - for i in (0..current_idx).rev() { 166 - if let Some((prev_span, prev_shape)) = shapes.get(i) { 167 - let prev_local_span = to_local_span(*prev_span); 168 - 169 - // Check if there's a separator between this shape and the next one 170 - let next_shape_start = if i + 1 < shapes.len() { 171 - to_local_span(shapes[i + 1].0).start 172 - } else { 173 - current_local_span.start 174 - }; 175 - 176 - if has_separator_between(prev_local_span.end, next_shape_start) { 177 - break; // Stop at separator 178 - } 179 - 180 - if is_command_shape(prev_shape, prev_local_span) { 181 - // Found the command 182 - let cmd_text = safe_slice(prev_local_span); 183 - // Extract just the command name (first word, no flags) 184 - let cmd_name = cmd_text 185 - .split_whitespace() 186 - .next() 187 - .unwrap_or(&cmd_text) 188 - .trim(); 189 - command_name = Some(cmd_name.to_string()); 190 - break; 191 - } else { 192 - // This is an argument - count it if it's not a flag 193 - let arg_text = safe_slice(prev_local_span); 194 - let trimmed_arg = arg_text.trim(); 195 - // Don't count flags (starting with -) or empty arguments 196 - if !trimmed_arg.is_empty() && !trimmed_arg.starts_with('-') { 197 - arg_count += 1; 198 - } 199 - } 200 - } 201 - } 202 - 203 - command_name.map(|name| (name, arg_count)) 204 - }; 205 - 206 - // Helper function to handle both Block and Closure shapes 207 - let handle_block_or_closure = |prefix: &str, 208 - span: Span, 209 - shape_name: &str, 210 - current_idx: usize, 211 - local_span: Span| 212 - -> Option<CompletionContext> { 213 - web_sys::console::log_1(&JsValue::from_str(&format!( 214 - "[completion] Processing {} shape with prefix: {:?}", 215 - shape_name, prefix 216 - ))); 217 - 218 - // Check if the content ends with a pipe or semicolon 219 - let prefix_ends_with_separator = ends_with_separator(prefix); 220 - let last_sep_pos_in_prefix = if prefix_ends_with_separator { 221 - find_last_separator_pos(prefix) 222 - } else { 223 - None 224 - }; 225 - web_sys::console::log_1(&JsValue::from_str(&format!( 226 - "[completion] {}: prefix_ends_with_separator={}, last_sep_pos_in_prefix={:?}", 227 - shape_name, prefix_ends_with_separator, last_sep_pos_in_prefix 228 - ))); 229 - 230 - if let Some((trimmed_prefix, adjusted_span, is_empty)) = handle_block_prefix(prefix, span) { 231 - web_sys::console::log_1(&JsValue::from_str(&format!( 232 - "[completion] {}: trimmed_prefix={:?}, is_empty={}", 233 - shape_name, trimmed_prefix, is_empty 234 - ))); 235 - 236 - if is_empty { 237 - // Empty block/closure or just whitespace - command context 238 - web_sys::console::log_1(&JsValue::from_str(&format!( 239 - "[completion] {} is empty, setting Command context", 240 - shape_name 241 - ))); 242 - Some(CompletionContext::Command { 243 - prefix: String::new(), 244 - span: adjusted_span, 245 - }) 246 - } else if let Some(last_sep_pos) = last_sep_pos_in_prefix { 247 - // After a separator - command context 248 - let after_sep = prefix[last_sep_pos..].trim_start(); 249 - web_sys::console::log_1(&JsValue::from_str(&format!( 250 - "[completion] {} has separator at {}, after_sep={:?}, setting Command context", 251 - shape_name, last_sep_pos, after_sep 252 - ))); 253 - Some(CompletionContext::Command { 254 - prefix: after_sep.to_string(), 255 - span: Span::new(span.start + last_sep_pos, span.end), 256 - }) 257 - } else { 258 - web_sys::console::log_1(&JsValue::from_str(&format!( 259 - "[completion] {} has no separator, checking for variable/flag/argument context", 260 - shape_name 261 - ))); 262 - // Check if this is a variable or cell path first 263 - let trimmed = trimmed_prefix.trim(); 264 - 265 - if trimmed.starts_with('$') { 266 - // Variable or cell path completion 267 - if let Some(dot_pos) = trimmed[1..].find('.') { 268 - // Cell path completion: $in.name, $env.PWD, etc. 269 - let var_name = &trimmed[1..dot_pos + 1]; 270 - let after_var = &trimmed[dot_pos + 2..]; 271 - let parts: Vec<&str> = after_var.split('.').collect(); 272 - let (path_so_far, cell_prefix) = if parts.is_empty() { 273 - (vec![], String::new()) 274 - } else if after_var.ends_with('.') { 275 - ( 276 - parts 277 - .iter() 278 - .filter(|s| !s.is_empty()) 279 - .map(|s| s.to_string()) 280 - .collect(), 281 - String::new(), 282 - ) 283 - } else { 284 - let path: Vec<String> = parts[..parts.len().saturating_sub(1)] 285 - .iter() 286 - .map(|s| s.to_string()) 287 - .collect(); 288 - let prefix = parts.last().map(|s| s.to_string()).unwrap_or_default(); 289 - (path, prefix) 290 - }; 291 - 292 - let var_id = match var_name { 293 - "env" => Some(ENV_VARIABLE_ID), 294 - "nu" => Some(NU_VARIABLE_ID), 295 - "in" => Some(IN_VARIABLE_ID), 296 - _ => working_set.find_variable(var_name.as_bytes()), 297 - }; 298 - 299 - if let Some(var_id) = var_id { 300 - let prefix_byte_len = cell_prefix.len(); 301 - let cell_span_start = adjusted_span.end.saturating_sub(prefix_byte_len); 302 - web_sys::console::log_1(&JsValue::from_str(&format!( 303 - "[completion] {}: Setting CellPath context with var {:?}, prefix {:?}", 304 - shape_name, var_name, cell_prefix 305 - ))); 306 - Some(CompletionContext::CellPath { 307 - prefix: cell_prefix, 308 - span: Span::new(cell_span_start, adjusted_span.end), 309 - var_id, 310 - path_so_far, 311 - }) 312 - } else { 313 - // Unknown variable, fall back to variable completion 314 - let var_prefix = trimmed[1..].to_string(); 315 - web_sys::console::log_1(&JsValue::from_str(&format!( 316 - "[completion] {}: Unknown var, setting Variable context with prefix {:?}", 317 - shape_name, var_prefix 318 - ))); 319 - Some(CompletionContext::Variable { 320 - prefix: var_prefix, 321 - span: adjusted_span, 322 - }) 323 - } 324 - } else { 325 - // Simple variable completion (no dot) 326 - let var_prefix = if trimmed.len() > 1 { 327 - trimmed[1..].to_string() 328 - } else { 329 - String::new() 330 - }; 331 - web_sys::console::log_1(&JsValue::from_str(&format!( 332 - "[completion] {}: Setting Variable context with prefix {:?}", 333 - shape_name, var_prefix 334 - ))); 335 - Some(CompletionContext::Variable { 336 - prefix: var_prefix, 337 - span: adjusted_span, 338 - }) 339 - } 340 - } else if trimmed.starts_with('-') { 341 - // Flag completion 342 - if let Some((cmd_name, _)) = find_command_and_arg_index(current_idx, local_span) 343 - { 344 - web_sys::console::log_1(&JsValue::from_str(&format!( 345 - "[completion] {}: Found command {:?} for flag completion", 346 - shape_name, cmd_name 347 - ))); 348 - Some(CompletionContext::Flag { 349 - prefix: trimmed.to_string(), 350 - span: adjusted_span, 351 - command_name: cmd_name, 352 - }) 353 - } else { 354 - Some(CompletionContext::Argument { 355 - prefix: trimmed_prefix, 356 - span: adjusted_span, 357 - }) 358 - } 359 - } else { 360 - // Try to find the command and argument index 361 - if let Some((cmd_name, arg_index)) = 362 - find_command_and_arg_index(current_idx, local_span) 363 - { 364 - web_sys::console::log_1(&JsValue::from_str(&format!( 365 - "[completion] {}: Found command {:?} with arg_index {} for argument completion", 366 - shape_name, cmd_name, arg_index 367 - ))); 368 - Some(CompletionContext::CommandArgument { 369 - prefix: trimmed.to_string(), 370 - span: adjusted_span, 371 - command_name: cmd_name, 372 - arg_index, 373 - }) 374 - } else { 375 - // No command found, treat as regular argument 376 - web_sys::console::log_1(&JsValue::from_str(&format!( 377 - "[completion] {}: No command found, using Argument context", 378 - shape_name 379 - ))); 380 - Some(CompletionContext::Argument { 381 - prefix: trimmed_prefix, 382 - span: adjusted_span, 383 - }) 384 - } 385 - } 386 - } 387 - } else { 388 - None 389 - } 390 - }; 391 - 392 - // Helper function to evaluate a variable for completion 393 - // Returns the Value of a variable if it can be evaluated 394 - let eval_variable_for_completion = |var_id: nu_protocol::VarId, 395 - working_set: &StateWorkingSet| 396 - -> Option<Value> { 397 - match var_id { 398 - id if id == NU_VARIABLE_ID => { 399 - // $nu - get from engine state constant 400 - engine_guard.get_constant(id).cloned() 401 - } 402 - id if id == ENV_VARIABLE_ID => { 403 - // $env - build from environment variables in engine state 404 - // EnvVars is HashMap<String, HashMap<String, Value>> (overlay -> vars) 405 - let mut pairs: Vec<(String, Value)> = Vec::new(); 406 - for overlay_env in engine_guard.env_vars.values() { 407 - for (name, value) in overlay_env.iter() { 408 - pairs.push((name.clone(), value.clone())); 409 - } 410 - } 411 - pairs.sort_by(|a, b| a.0.cmp(&b.0)); 412 - // Deduplicate by name (later overlays override earlier ones) 413 - pairs.dedup_by(|a, b| a.0 == b.0); 414 - Some(Value::record(pairs.into_iter().collect(), Span::unknown())) 415 - } 416 - id if id == IN_VARIABLE_ID => { 417 - // $in - typically not available at completion time 418 - None 419 - } 420 - _ => { 421 - // User-defined variable - try to get const value first 422 - let var_info = working_set.get_variable(var_id); 423 - if let Some(const_val) = &var_info.const_val { 424 - Some(const_val.clone()) 425 - } else { 426 - // Variable doesn't have a const value (runtime value) 427 - // Try to get the value from the stack (runtime storage) 428 - match stack_guard.get_var(var_id, Span::unknown()) { 429 - Ok(value) => { 430 - web_sys::console::log_1(&JsValue::from_str(&format!( 431 - "[completion] Found variable {:?} value in stack", 432 - var_id 433 - ))); 434 - Some(value) 435 - } 436 - Err(_) => { 437 - // Variable not in stack either 438 - web_sys::console::log_1(&JsValue::from_str(&format!( 439 - "[completion] Variable {:?} has no const value and not in stack, type: {:?}", 440 - var_id, var_info.ty 441 - ))); 442 - None 443 - } 444 - } 445 - } 446 - } 447 - } 448 - }; 449 - 450 - // Helper function to extract column/field names from a Value 451 - let get_columns_from_value = |value: &Value| -> Vec<(String, Option<String>)> { 452 - match value { 453 - Value::Record { val, .. } => val 454 - .iter() 455 - .map(|(name, v)| (name.to_string(), Some(v.get_type().to_string()))) 456 - .collect(), 457 - Value::List { vals, .. } => { 458 - // Get common columns from list of records 459 - if let Some(first) = vals.first() { 460 - if let Value::Record { val, .. } = first { 461 - return val 462 - .iter() 463 - .map(|(name, v)| (name.to_string(), Some(v.get_type().to_string()))) 464 - .collect(); 465 - } 466 - } 467 - vec![] 468 - } 469 - _ => vec![], 470 - } 471 - }; 472 - 473 - // Helper function to follow a cell path and get the value at that path 474 - let follow_cell_path = |value: &Value, path: &[String]| -> Option<Value> { 475 - let mut current = value.clone(); 476 - for member in path { 477 - match &current { 478 - Value::Record { val, .. } => { 479 - current = val.get(member)?.clone(); 480 - } 481 - Value::List { vals, .. } => { 482 - // Try to parse as index or get from first record 483 - if let Ok(idx) = member.parse::<usize>() { 484 - current = vals.get(idx)?.clone(); 485 - } else if let Some(first) = vals.first() { 486 - if let Value::Record { val, .. } = first { 487 - current = val.get(member)?.clone(); 488 - } else { 489 - return None; 490 - } 491 - } else { 492 - return None; 493 - } 494 - } 495 - _ => return None, 496 - } 497 - } 498 - Some(current) 499 - }; 500 - 501 - // Helper function to extract closure parameters from input string at cursor position 502 - // We parse the input directly to find closures containing the cursor and extract their parameters 503 - let extract_closure_params = |input: &str, cursor_pos: usize| -> Vec<String> { 504 - let mut params = Vec::new(); 505 - 506 - // Find all closures in the input by looking for {|...| patterns 507 - // We need to find closures that contain the cursor position 508 - let mut brace_stack: Vec<usize> = Vec::new(); // Stack of opening brace positions 509 - let mut closures: Vec<(usize, usize, Vec<String>)> = Vec::new(); // (start, end, params) 510 - 511 - let mut i = 0; 512 - let chars: Vec<char> = input.chars().collect(); 513 - 514 - while i < chars.len() { 515 - if chars[i] == '{' { 516 - brace_stack.push(i); 517 - } else if chars[i] == '}' { 518 - if let Some(start) = brace_stack.pop() { 519 - // Check if this is a closure with parameters: {|param| ...} 520 - if start + 1 < chars.len() && chars[start + 1] == '|' { 521 - // Find the parameter list 522 - let param_start = start + 2; 523 - let mut param_end = param_start; 524 - 525 - // Find the closing | of the parameter list 526 - while param_end < chars.len() && chars[param_end] != '|' { 527 - param_end += 1; 528 - } 529 - 530 - if param_end < chars.len() { 531 - // Extract parameter names 532 - let params_text: String = 533 - chars[param_start..param_end].iter().collect(); 534 - let param_names: Vec<String> = params_text 535 - .split(',') 536 - .map(|s| s.trim().to_string()) 537 - .filter(|s| !s.is_empty()) 538 - .collect(); 539 - 540 - closures.push((start, i + 1, param_names)); 541 - } 542 - } 543 - } 544 - } 545 - i += 1; 546 - } 547 - 548 - // Find closures that contain the cursor position 549 - // A closure contains the cursor if: start <= cursor_pos < end 550 - for (start, end, param_names) in closures { 551 - if start <= cursor_pos && cursor_pos < end { 552 - web_sys::console::log_1(&JsValue::from_str(&format!( 553 - "[completion] Found closure at [{}, {}) containing cursor {}, params: {:?}", 554 - start, end, cursor_pos, param_names 555 - ))); 556 - params.extend(param_names); 557 - } 558 - } 559 - 560 - params 561 - }; 562 - 563 - // Helper function to collect variables from working set 564 - let collect_variables = |working_set: &StateWorkingSet, 565 - input: &str, 566 - cursor_pos: usize| 567 - -> HashMap<String, nu_protocol::VarId> { 568 - let mut variables = HashMap::new(); 569 - 570 - // Add built-in variables 571 - variables.insert("$nu".to_string(), NU_VARIABLE_ID); 572 - variables.insert("$in".to_string(), IN_VARIABLE_ID); 573 - variables.insert("$env".to_string(), ENV_VARIABLE_ID); 574 - 575 - // Collect closure parameters at cursor position 576 - // We don't need real var_ids for closure parameters since they're not evaluated yet 577 - // We'll use a placeholder var_id (using IN_VARIABLE_ID as a safe placeholder) 578 - // The actual var_id lookup will happen when the variable is used 579 - let closure_params = extract_closure_params(input, cursor_pos); 580 - for param_name in closure_params { 581 - let var_name = format!("${}", param_name); 582 - // Use IN_VARIABLE_ID as placeholder - it's safe since we're just using it for the name 583 - // The completion logic only needs the name, not the actual var_id 584 - variables.insert(var_name.clone(), IN_VARIABLE_ID); 585 - web_sys::console::log_1(&JsValue::from_str(&format!( 586 - "[completion] Added closure parameter: {:?}", 587 - var_name 588 - ))); 589 - } 590 - 591 - // Collect from working set delta scope 592 - let mut removed_overlays = vec![]; 593 - for scope_frame in working_set.delta.scope.iter().rev() { 594 - for overlay_frame in scope_frame.active_overlays(&mut removed_overlays).rev() { 595 - for (name, var_id) in &overlay_frame.vars { 596 - let name = String::from_utf8_lossy(name).to_string(); 597 - variables.insert(name, *var_id); 598 - } 599 - } 600 - } 601 - 602 - // Collect from permanent state scope 603 - for overlay_frame in working_set 604 - .permanent_state 605 - .active_overlays(&removed_overlays) 606 - .rev() 607 - { 608 - for (name, var_id) in &overlay_frame.vars { 609 - let name = String::from_utf8_lossy(name).to_string(); 610 - variables.insert(name, *var_id); 611 - } 612 - } 613 - 614 - variables 615 - }; 616 - 617 - // Find what we're completing 618 - #[derive(Debug)] 619 - enum CompletionContext { 620 - Command { 621 - prefix: String, 622 - span: Span, 623 - }, 624 - Argument { 625 - prefix: String, 626 - span: Span, 627 - }, 628 - Flag { 629 - prefix: String, 630 - span: Span, 631 - command_name: String, 632 - }, 633 - CommandArgument { 634 - prefix: String, 635 - span: Span, 636 - command_name: String, 637 - arg_index: usize, 638 - }, 639 - Variable { 640 - prefix: String, // without the $ prefix 641 - span: Span, 642 - }, 643 - CellPath { 644 - prefix: String, // the partial field name being typed (after the last dot) 645 - span: Span, // replacement span 646 - var_id: nu_protocol::VarId, // variable ID for evaluation 647 - path_so_far: Vec<String>, // path members accessed before current one 648 - }, 649 - } 650 - 651 - let mut context: Option<CompletionContext> = None; 652 - 653 - // Helper function to build full command prefix by looking backwards through shapes 654 - let build_command_prefix = 655 - |current_idx: usize, current_local_span: Span, current_prefix: &str| -> (String, Span) { 656 - let mut span_start = current_local_span.start; 657 - 658 - // Look backwards through shapes to find previous command words 659 - for i in (0..current_idx).rev() { 660 - if let Some((prev_span, prev_shape)) = shapes.get(i) { 661 - let prev_local_span = to_local_span(*prev_span); 662 - 663 - if is_command_shape(prev_shape, prev_local_span) { 664 - // Check if there's a separator between this shape and the next one 665 - let next_shape_start = if i + 1 < shapes.len() { 666 - to_local_span(shapes[i + 1].0).start 667 - } else { 668 - current_local_span.start 669 - }; 670 - 671 - // Check if there's a separator (pipe, semicolon, etc.) between shapes 672 - // Whitespace is fine, but separators indicate a new command 673 - if has_separator_between(prev_local_span.end, next_shape_start) { 674 - break; // Stop at separator 675 - } 676 - 677 - // Update span start to include this command word 678 - span_start = prev_local_span.start; 679 - } else { 680 - // Not a command shape, stop looking backwards 681 - break; 682 - } 683 - } 684 - } 685 - 686 - // Extract the full prefix from the input, preserving exact spacing 687 - let span_end = current_local_span.end; 688 - let full_prefix = if span_start < input.len() { 689 - safe_slice(Span::new(span_start, span_end)) 690 - } else { 691 - current_prefix.to_string() 692 - }; 693 - 694 - (full_prefix, Span::new(span_start, span_end)) 695 - }; 696 - 697 - // Helper function to get command signature (needed for context determination) 698 - let get_command_signature = |cmd_name: &str| -> Option<nu_protocol::Signature> { 699 - engine_guard 700 - .find_decl(cmd_name.as_bytes(), &[]) 701 - .map(|id| engine_guard.get_decl(id).signature()) 702 - }; 703 - 704 - // First, check if cursor is within a shape 705 - for (idx, (span, shape)) in shapes.iter().enumerate() { 706 - let local_span = to_local_span(*span); 707 - 708 - if local_span.start <= byte_pos && byte_pos <= local_span.end { 709 - web_sys::console::log_1(&JsValue::from_str(&format!( 710 - "[completion] Cursor in shape {}: {:?} at {:?}", 711 - idx, shape, local_span 712 - ))); 713 - 714 - // Check if there's a pipe or semicolon between this shape's end and the cursor 715 - // If so, we're starting a new command and should ignore this shape 716 - let has_sep = has_separator_between(local_span.end, byte_pos); 717 - if has_sep { 718 - web_sys::console::log_1(&JsValue::from_str(&format!( 719 - "[completion] Separator found between shape end ({}) and cursor ({}), skipping shape", 720 - local_span.end, byte_pos 721 - ))); 722 - // There's a separator, so we're starting a new command - skip this shape 723 - continue; 724 - } 725 - 726 - let span = Span::new(local_span.start, std::cmp::min(local_span.end, byte_pos)); 727 - let prefix = safe_slice(span); 728 - web_sys::console::log_1(&JsValue::from_str(&format!( 729 - "[completion] Processing shape {} with prefix: {:?}", 730 - idx, prefix 731 - ))); 732 - 733 - // Special case: if prefix is just '{' (possibly with whitespace), 734 - // we're at the start of a block and should complete commands 735 - let trimmed_prefix = prefix.trim(); 736 - if trimmed_prefix == "{" { 737 - // We're right after '{' - command context 738 - if let Some((_, adjusted_span, _)) = handle_block_prefix(&prefix, span) { 739 - context = Some(CompletionContext::Command { 740 - prefix: String::new(), 741 - span: adjusted_span, 742 - }); 743 - } 744 - } else { 745 - match shape { 746 - // Special case: Check if we're completing a cell path where the Variable and field are in separate shapes 747 - // e.g., `$a.na` where $a is a Variable shape and `.na` is a String shape 748 - _ if { idx > 0 && matches!(shape, FlatShape::String) } => { 749 - // Look at the previous shape to see if it's a Variable 750 - let prev_shape = &shapes[idx - 1]; 751 - let prev_local_span = to_local_span(prev_shape.0); 752 - 753 - if let FlatShape::Variable(var_id) = prev_shape.1 { 754 - // Check if the variable shape ends right where this shape starts (or very close) 755 - // Allow for a small gap (like a dot) between shapes 756 - let gap = local_span.start.saturating_sub(prev_local_span.end); 757 - if gap <= 1 { 758 - // This is a cell path - the String shape contains the field name(s) 759 - // The prefix might be like "na" or "field.subfield" 760 - let trimmed_prefix = prefix.trim(); 761 - let parts: Vec<&str> = trimmed_prefix.split('.').collect(); 762 - let (path_so_far, cell_prefix) = if parts.is_empty() { 763 - (vec![], String::new()) 764 - } else if trimmed_prefix.ends_with('.') { 765 - ( 766 - parts 767 - .iter() 768 - .filter(|s| !s.is_empty()) 769 - .map(|s| s.to_string()) 770 - .collect(), 771 - String::new(), 772 - ) 773 - } else { 774 - let path: Vec<String> = parts[..parts.len().saturating_sub(1)] 775 - .iter() 776 - .map(|s| s.to_string()) 777 - .collect(); 778 - let prefix = 779 - parts.last().map(|s| s.to_string()).unwrap_or_default(); 780 - (path, prefix) 781 - }; 782 - 783 - let prefix_byte_len = cell_prefix.len(); 784 - let cell_span_start = span.end.saturating_sub(prefix_byte_len); 785 - web_sys::console::log_1(&JsValue::from_str(&format!( 786 - "[completion] Detected cell path from Variable+String shapes, var_id={:?}, prefix={:?}, path={:?}", 787 - var_id, cell_prefix, path_so_far 788 - ))); 789 - context = Some(CompletionContext::CellPath { 790 - prefix: cell_prefix, 791 - span: Span::new(cell_span_start, span.end), 792 - var_id, 793 - path_so_far, 794 - }); 795 - } else { 796 - // Gap between shapes, check if this is a flag 797 - let trimmed_prefix = prefix.trim(); 798 - if trimmed_prefix.starts_with('-') { 799 - // This looks like a flag - find the command 800 - if let Some((cmd_name, _)) = 801 - find_command_and_arg_index(idx, local_span) 802 - { 803 - context = Some(CompletionContext::Flag { 804 - prefix: trimmed_prefix.to_string(), 805 - span, 806 - command_name: cmd_name, 807 - }); 808 - } else { 809 - context = 810 - Some(CompletionContext::Argument { prefix, span }); 811 - } 812 - } else { 813 - context = Some(CompletionContext::Argument { prefix, span }); 814 - } 815 - } 816 - } else { 817 - // Previous shape is not a Variable, check if this is a flag 818 - let trimmed_prefix = prefix.trim(); 819 - if trimmed_prefix.starts_with('-') { 820 - // This looks like a flag - find the command 821 - if let Some((cmd_name, _)) = 822 - find_command_and_arg_index(idx, local_span) 823 - { 824 - context = Some(CompletionContext::Flag { 825 - prefix: trimmed_prefix.to_string(), 826 - span, 827 - command_name: cmd_name, 828 - }); 829 - } else { 830 - context = Some(CompletionContext::Argument { prefix, span }); 831 - } 832 - } else { 833 - // This is likely a regular string argument 834 - context = Some(CompletionContext::Argument { prefix, span }); 835 - } 836 - } 837 - } 838 - // Special case: Check if we're completing a cell path where the Variable and dot are in separate shapes 839 - // e.g., `{ $in. }` where $in is Shape 4 (Variable) and `. }` is Shape 5 (Block) 840 - _ if { 841 - let trimmed_prefix = prefix.trim(); 842 - trimmed_prefix.starts_with('.') && idx > 0 843 - } => 844 - { 845 - // Look at the previous shape to see if it's a Variable 846 - let prev_shape = &shapes[idx - 1]; 847 - let prev_local_span = to_local_span(prev_shape.0); 848 - 849 - if let FlatShape::Variable(var_id) = prev_shape.1 { 850 - // Check if the variable shape ends right where this shape starts 851 - if prev_local_span.end == local_span.start { 852 - let trimmed_prefix = prefix.trim(); 853 - // Parse path members from the prefix (which is like ".field" or ".field.subfield") 854 - let after_dot = &trimmed_prefix[1..]; // Remove leading dot 855 - let parts: Vec<&str> = after_dot.split('.').collect(); 856 - let (path_so_far, cell_prefix) = if parts.is_empty() 857 - || (parts.len() == 1 && parts[0].is_empty()) 858 - { 859 - (vec![], String::new()) 860 - } else if after_dot.ends_with('.') { 861 - ( 862 - parts 863 - .iter() 864 - .filter(|s| !s.is_empty()) 865 - .map(|s| s.to_string()) 866 - .collect(), 867 - String::new(), 868 - ) 869 - } else { 870 - let path: Vec<String> = parts[..parts.len().saturating_sub(1)] 871 - .iter() 872 - .map(|s| s.to_string()) 873 - .collect(); 874 - let prefix = 875 - parts.last().map(|s| s.to_string()).unwrap_or_default(); 876 - (path, prefix) 877 - }; 878 - 879 - let prefix_byte_len = cell_prefix.len(); 880 - let cell_span_start = span.end.saturating_sub(prefix_byte_len); 881 - web_sys::console::log_1(&JsValue::from_str(&format!( 882 - "[completion] Detected cell path from adjacent Variable shape, var_id={:?}, prefix={:?}", 883 - var_id, cell_prefix 884 - ))); 885 - context = Some(CompletionContext::CellPath { 886 - prefix: cell_prefix, 887 - span: Span::new(cell_span_start, span.end), 888 - var_id, 889 - path_so_far, 890 - }); 891 - } else { 892 - // Gap between shapes, fall through to default handling 893 - context = Some(CompletionContext::Argument { prefix, span }); 894 - } 895 - } else { 896 - // Previous shape is not a Variable, this is likely a file path starting with . 897 - context = Some(CompletionContext::Argument { prefix, span }); 898 - } 899 - } 900 - _ if { 901 - // Check if this is a variable or cell path (starts with $) before treating as command 902 - let trimmed_prefix = prefix.trim(); 903 - trimmed_prefix.starts_with('$') 904 - } => 905 - { 906 - let trimmed_prefix = prefix.trim(); 907 - // Check if this is a cell path (contains a dot after $) 908 - if let Some(dot_pos) = trimmed_prefix[1..].find('.') { 909 - // Cell path completion: $env.PWD, $nu.home-path, etc. 910 - let var_name = &trimmed_prefix[1..dot_pos + 1]; // e.g., "env" 911 - let after_var = &trimmed_prefix[dot_pos + 2..]; // e.g., "PWD" or "config.color" 912 - 913 - // Parse path members and current prefix 914 - let parts: Vec<&str> = after_var.split('.').collect(); 915 - let (path_so_far, cell_prefix) = if parts.is_empty() { 916 - (vec![], String::new()) 917 - } else if after_var.ends_with('.') { 918 - // Cursor is right after a dot, complete all fields 919 - ( 920 - parts 921 - .iter() 922 - .filter(|s| !s.is_empty()) 923 - .map(|s| s.to_string()) 924 - .collect(), 925 - String::new(), 926 - ) 927 - } else { 928 - // Cursor is in the middle of typing a field name 929 - let path: Vec<String> = parts[..parts.len().saturating_sub(1)] 930 - .iter() 931 - .map(|s| s.to_string()) 932 - .collect(); 933 - let prefix = 934 - parts.last().map(|s| s.to_string()).unwrap_or_default(); 935 - (path, prefix) 936 - }; 937 - 938 - // Find the variable ID 939 - let var_id = match var_name { 940 - "env" => Some(ENV_VARIABLE_ID), 941 - "nu" => Some(NU_VARIABLE_ID), 942 - "in" => Some(IN_VARIABLE_ID), 943 - _ => { 944 - // Try to find user-defined variable 945 - working_set.find_variable(var_name.as_bytes()) 946 - } 947 - }; 948 - 949 - if let Some(var_id) = var_id { 950 - // Calculate span for the cell path member being completed 951 - let prefix_byte_len = cell_prefix.len(); 952 - let cell_span_start = span.end.saturating_sub(prefix_byte_len); 953 - context = Some(CompletionContext::CellPath { 954 - prefix: cell_prefix, 955 - span: Span::new(cell_span_start, span.end), 956 - var_id, 957 - path_so_far, 958 - }); 959 - } else { 960 - // Unknown variable, fall back to variable completion 961 - let var_prefix = trimmed_prefix[1..].to_string(); 962 - context = Some(CompletionContext::Variable { 963 - prefix: var_prefix, 964 - span, 965 - }); 966 - } 967 - } else { 968 - // Variable completion context (no dot) 969 - let var_prefix = if trimmed_prefix.len() > 1 { 970 - trimmed_prefix[1..].to_string() 971 - } else { 972 - String::new() 973 - }; 974 - context = Some(CompletionContext::Variable { 975 - prefix: var_prefix, 976 - span, 977 - }); 978 - } 979 - } 980 - _ if is_command_shape(shape, local_span) => { 981 - let (full_prefix, full_span) = build_command_prefix(idx, span, &prefix); 982 - context = Some(CompletionContext::Command { 983 - prefix: full_prefix, 984 - span: full_span, 985 - }); 986 - } 987 - FlatShape::Block | FlatShape::Closure => { 988 - if let Some(ctx) = handle_block_or_closure( 989 - &prefix, 990 - span, 991 - shape.as_str().trim_start_matches("shape_"), 992 - idx, 993 - local_span, 994 - ) { 995 - context = Some(ctx); 996 - } 997 - } 998 - FlatShape::Variable(var_id) => { 999 - // Variable or cell path completion context 1000 - let trimmed_prefix = prefix.trim(); 1001 - if trimmed_prefix.starts_with('$') { 1002 - // Check if this is a cell path (contains a dot after $) 1003 - if let Some(dot_pos) = trimmed_prefix[1..].find('.') { 1004 - // Cell path completion 1005 - let after_var = &trimmed_prefix[dot_pos + 2..]; 1006 - let parts: Vec<&str> = after_var.split('.').collect(); 1007 - let (path_so_far, cell_prefix) = if parts.is_empty() { 1008 - (vec![], String::new()) 1009 - } else if after_var.ends_with('.') { 1010 - ( 1011 - parts 1012 - .iter() 1013 - .filter(|s| !s.is_empty()) 1014 - .map(|s| s.to_string()) 1015 - .collect(), 1016 - String::new(), 1017 - ) 1018 - } else { 1019 - let path: Vec<String> = parts[..parts.len().saturating_sub(1)] 1020 - .iter() 1021 - .map(|s| s.to_string()) 1022 - .collect(); 1023 - let prefix = 1024 - parts.last().map(|s| s.to_string()).unwrap_or_default(); 1025 - (path, prefix) 1026 - }; 1027 - 1028 - let prefix_byte_len = cell_prefix.len(); 1029 - let cell_span_start = span.end.saturating_sub(prefix_byte_len); 1030 - context = Some(CompletionContext::CellPath { 1031 - prefix: cell_prefix, 1032 - span: Span::new(cell_span_start, span.end), 1033 - var_id: *var_id, 1034 - path_so_far, 1035 - }); 1036 - } else { 1037 - // Simple variable completion 1038 - let var_prefix = trimmed_prefix[1..].to_string(); 1039 - context = Some(CompletionContext::Variable { 1040 - prefix: var_prefix, 1041 - span, 1042 - }); 1043 - } 1044 - } else { 1045 - // Fallback to argument context if no $ found 1046 - context = Some(CompletionContext::Argument { prefix, span }); 1047 - } 1048 - } 1049 - _ => { 1050 - // Check if this is a variable or cell path (starts with $) 1051 - let trimmed_prefix = prefix.trim(); 1052 - if trimmed_prefix.starts_with('$') { 1053 - // Check if this is a cell path (contains a dot after $) 1054 - if let Some(dot_pos) = trimmed_prefix[1..].find('.') { 1055 - // Cell path completion 1056 - let var_name = &trimmed_prefix[1..dot_pos + 1]; 1057 - let after_var = &trimmed_prefix[dot_pos + 2..]; 1058 - let parts: Vec<&str> = after_var.split('.').collect(); 1059 - let (path_so_far, cell_prefix) = if parts.is_empty() { 1060 - (vec![], String::new()) 1061 - } else if after_var.ends_with('.') { 1062 - ( 1063 - parts 1064 - .iter() 1065 - .filter(|s| !s.is_empty()) 1066 - .map(|s| s.to_string()) 1067 - .collect(), 1068 - String::new(), 1069 - ) 1070 - } else { 1071 - let path: Vec<String> = parts[..parts.len().saturating_sub(1)] 1072 - .iter() 1073 - .map(|s| s.to_string()) 1074 - .collect(); 1075 - let prefix = 1076 - parts.last().map(|s| s.to_string()).unwrap_or_default(); 1077 - (path, prefix) 1078 - }; 1079 - 1080 - let var_id = match var_name { 1081 - "env" => Some(ENV_VARIABLE_ID), 1082 - "nu" => Some(NU_VARIABLE_ID), 1083 - "in" => Some(IN_VARIABLE_ID), 1084 - _ => working_set.find_variable(var_name.as_bytes()), 1085 - }; 1086 - 1087 - if let Some(var_id) = var_id { 1088 - let prefix_byte_len = cell_prefix.len(); 1089 - let cell_span_start = span.end.saturating_sub(prefix_byte_len); 1090 - context = Some(CompletionContext::CellPath { 1091 - prefix: cell_prefix, 1092 - span: Span::new(cell_span_start, span.end), 1093 - var_id, 1094 - path_so_far, 1095 - }); 1096 - } else { 1097 - let var_prefix = trimmed_prefix[1..].to_string(); 1098 - context = Some(CompletionContext::Variable { 1099 - prefix: var_prefix, 1100 - span, 1101 - }); 1102 - } 1103 - } else { 1104 - // Simple variable completion 1105 - let var_prefix = if trimmed_prefix.len() > 1 { 1106 - trimmed_prefix[1..].to_string() 1107 - } else { 1108 - String::new() 1109 - }; 1110 - context = Some(CompletionContext::Variable { 1111 - prefix: var_prefix, 1112 - span, 1113 - }); 1114 - } 1115 - } else if trimmed_prefix.starts_with('-') { 1116 - // This looks like a flag - find the command 1117 - if let Some((cmd_name, _)) = find_command_and_arg_index(idx, local_span) 1118 - { 1119 - context = Some(CompletionContext::Flag { 1120 - prefix: trimmed_prefix.to_string(), 1121 - span, 1122 - command_name: cmd_name, 1123 - }); 1124 - } else { 1125 - context = Some(CompletionContext::Argument { prefix, span }); 1126 - } 1127 - } else { 1128 - // This is a positional argument - find the command and argument index 1129 - if let Some((cmd_name, arg_index)) = 1130 - find_command_and_arg_index(idx, local_span) 1131 - { 1132 - context = Some(CompletionContext::CommandArgument { 1133 - prefix: trimmed_prefix.to_string(), 1134 - span, 1135 - command_name: cmd_name, 1136 - arg_index, 1137 - }); 1138 - } else { 1139 - context = Some(CompletionContext::Argument { prefix, span }); 1140 - } 1141 - } 1142 - } 1143 - } 1144 - } 1145 - break; 1146 - } 1147 - } 1148 - 1149 - // If not in a shape, check what comes before the cursor 1150 - if context.is_none() { 1151 - web_sys::console::log_1(&JsValue::from_str( 1152 - "[completion] Context is None, entering fallback logic", 1153 - )); 1154 - // Check if there's a command-like shape before us 1155 - let mut found_command_before = false; 1156 - let mut has_separator_after_command = false; 1157 - for (span, shape) in shapes.iter().rev() { 1158 - let local_span = to_local_span(*span); 1159 - if local_span.end <= byte_pos { 1160 - if is_command_shape(shape, local_span) { 1161 - // Check if there's a pipe or semicolon between this command and the cursor 1162 - has_separator_after_command = has_separator_between(local_span.end, byte_pos); 1163 - web_sys::console::log_1(&JsValue::from_str(&format!( 1164 - "[completion] Found command shape {:?} at {:?}, has_separator_after_command={}", 1165 - shape, local_span, has_separator_after_command 1166 - ))); 1167 - if !has_separator_after_command { 1168 - found_command_before = true; 1169 - 1170 - // Extract the command text 1171 - let cmd = safe_slice(local_span); 1172 - let cmd_name = cmd.split_whitespace().next().unwrap_or(&cmd).trim(); 1173 - 1174 - // Check if we're right after the command (only whitespace between command and cursor) 1175 - let text_after_command = if local_span.end < input.len() { 1176 - &input[local_span.end..byte_pos] 1177 - } else { 1178 - "" 1179 - }; 1180 - let is_right_after_command = text_after_command.trim().is_empty(); 1181 - 1182 - // If we're right after a command, check if it has positional arguments 1183 - if is_right_after_command { 1184 - if let Some(signature) = get_command_signature(cmd_name) { 1185 - // Check if command has any positional arguments 1186 - let has_positional_args = !signature.required_positional.is_empty() 1187 - || !signature.optional_positional.is_empty(); 1188 - 1189 - if has_positional_args { 1190 - // Count existing arguments before cursor 1191 - let mut arg_count = 0; 1192 - for (prev_span, prev_shape) in shapes.iter().rev() { 1193 - let prev_local_span = to_local_span(*prev_span); 1194 - if prev_local_span.end <= byte_pos 1195 - && prev_local_span.end > local_span.end 1196 - { 1197 - if !is_command_shape(prev_shape, prev_local_span) { 1198 - let arg_text = safe_slice(prev_local_span); 1199 - let trimmed_arg = arg_text.trim(); 1200 - // Don't count flags (starting with -) or empty arguments 1201 - if !trimmed_arg.is_empty() 1202 - && !trimmed_arg.starts_with('-') 1203 - { 1204 - arg_count += 1; 1205 - } 1206 - } 1207 - } 1208 - } 1209 - 1210 - web_sys::console::log_1(&JsValue::from_str(&format!( 1211 - "[completion] Right after command {:?}, setting CommandArgument context with arg_index: {}", 1212 - cmd_name, arg_count 1213 - ))); 1214 - 1215 - context = Some(CompletionContext::CommandArgument { 1216 - prefix: String::new(), 1217 - span: Span::new(byte_pos, byte_pos), 1218 - command_name: cmd_name.to_string(), 1219 - arg_index: arg_count, 1220 - }); 1221 - } else { 1222 - // No positional arguments, don't show any completions 1223 - web_sys::console::log_1(&JsValue::from_str(&format!( 1224 - "[completion] Command {:?} has no positional args, not showing completions", 1225 - cmd_name 1226 - ))); 1227 - // Leave context as None to show no completions 1228 - } 1229 - } else { 1230 - // Couldn't find signature, don't show completions 1231 - web_sys::console::log_1(&JsValue::from_str(&format!( 1232 - "[completion] Could not find signature for {:?}, not showing completions", 1233 - cmd_name 1234 - ))); 1235 - // Leave context as None to show no completions 1236 - } 1237 - } else { 1238 - // Not right after command, complete the command itself 1239 - web_sys::console::log_1(&JsValue::from_str(&format!( 1240 - "[completion] Set Command context with prefix: {:?}", 1241 - cmd 1242 - ))); 1243 - context = Some(CompletionContext::Command { 1244 - prefix: cmd, 1245 - span: local_span, 1246 - }); 1247 - } 1248 - } 1249 - } 1250 - break; 1251 - } 1252 - } 1253 - 1254 - if !found_command_before { 1255 - web_sys::console::log_1(&JsValue::from_str( 1256 - "[completion] No command found before cursor, checking tokens", 1257 - )); 1258 - // No command before, check context from tokens 1259 - let (tokens, _) = lex(input.as_bytes(), 0, &[], &[], true); 1260 - let last_token = tokens.iter().filter(|t| t.span.end <= byte_pos).last(); 1261 - 1262 - let is_cmd_context = if let Some(token) = last_token { 1263 - let matches = matches!( 1264 - token.contents, 1265 - TokenContents::Pipe 1266 - | TokenContents::PipePipe 1267 - | TokenContents::Semicolon 1268 - | TokenContents::Eol 1269 - ); 1270 - web_sys::console::log_1(&JsValue::from_str(&format!( 1271 - "[completion] Last token: {:?}, is_cmd_context from token={}", 1272 - token.contents, matches 1273 - ))); 1274 - matches 1275 - } else { 1276 - web_sys::console::log_1(&JsValue::from_str( 1277 - "[completion] No last token found, assuming start of input (is_cmd_context=true)", 1278 - )); 1279 - true // Start of input 1280 - }; 1281 - 1282 - // Look for the last non-whitespace token before cursor 1283 - let text_before = &input[..byte_pos]; 1284 - 1285 - // Also check if we're inside a block - if the last non-whitespace char before cursor is '{' 1286 - let text_before_trimmed = text_before.trim_end(); 1287 - let is_inside_block = text_before_trimmed.ends_with('{'); 1288 - // If we found a separator after a command, we're starting a new command 1289 - let is_cmd_context = is_cmd_context || is_inside_block || has_separator_after_command; 1290 - web_sys::console::log_1(&JsValue::from_str(&format!( 1291 - "[completion] is_inside_block={}, has_separator_after_command={}, final is_cmd_context={}", 1292 - is_inside_block, has_separator_after_command, is_cmd_context 1293 - ))); 1294 - 1295 - // Find the last word before cursor 1296 - let last_word_start = text_before 1297 - .rfind(|c: char| c.is_whitespace() || is_separator_char(c)) 1298 - .map(|i| i + 1) 1299 - .unwrap_or(0); 1300 - 1301 - let last_word = text_before[last_word_start..].trim_start(); 1302 - web_sys::console::log_1(&JsValue::from_str(&format!( 1303 - "[completion] last_word_start={}, last_word={:?}", 1304 - last_word_start, last_word 1305 - ))); 1306 - 1307 - if is_cmd_context { 1308 - context = Some(CompletionContext::Command { 1309 - prefix: last_word.to_string(), 1310 - span: Span::new(last_word_start, byte_pos), 1311 - }); 1312 - web_sys::console::log_1(&JsValue::from_str(&format!( 1313 - "[completion] Set Command context with prefix: {:?}", 1314 - last_word 1315 - ))); 1316 - } else { 1317 - // Check if this is a variable or cell path (starts with $) 1318 - let trimmed_word = last_word.trim(); 1319 - if trimmed_word.starts_with('$') { 1320 - // Check if this is a cell path (contains a dot after $) 1321 - if let Some(dot_pos) = trimmed_word[1..].find('.') { 1322 - // Cell path completion 1323 - let var_name = &trimmed_word[1..dot_pos + 1]; 1324 - let after_var = &trimmed_word[dot_pos + 2..]; 1325 - let parts: Vec<&str> = after_var.split('.').collect(); 1326 - let (path_so_far, cell_prefix) = if parts.is_empty() { 1327 - (vec![], String::new()) 1328 - } else if after_var.ends_with('.') { 1329 - ( 1330 - parts 1331 - .iter() 1332 - .filter(|s| !s.is_empty()) 1333 - .map(|s| s.to_string()) 1334 - .collect(), 1335 - String::new(), 1336 - ) 1337 - } else { 1338 - let path: Vec<String> = parts[..parts.len().saturating_sub(1)] 1339 - .iter() 1340 - .map(|s| s.to_string()) 1341 - .collect(); 1342 - let prefix = parts.last().map(|s| s.to_string()).unwrap_or_default(); 1343 - (path, prefix) 1344 - }; 1345 - 1346 - let var_id = match var_name { 1347 - "env" => Some(ENV_VARIABLE_ID), 1348 - "nu" => Some(NU_VARIABLE_ID), 1349 - "in" => Some(IN_VARIABLE_ID), 1350 - _ => working_set.find_variable(var_name.as_bytes()), 1351 - }; 1352 - 1353 - if let Some(var_id) = var_id { 1354 - let prefix_byte_len = cell_prefix.len(); 1355 - let cell_span_start = byte_pos.saturating_sub(prefix_byte_len); 1356 - let cell_prefix_clone = cell_prefix.clone(); 1357 - context = Some(CompletionContext::CellPath { 1358 - prefix: cell_prefix, 1359 - span: Span::new(cell_span_start, byte_pos), 1360 - var_id, 1361 - path_so_far, 1362 - }); 1363 - web_sys::console::log_1(&JsValue::from_str(&format!( 1364 - "[completion] Set CellPath context with prefix: {:?}", 1365 - cell_prefix_clone 1366 - ))); 1367 - } else { 1368 - let var_prefix = trimmed_word[1..].to_string(); 1369 - let var_prefix_clone = var_prefix.clone(); 1370 - context = Some(CompletionContext::Variable { 1371 - prefix: var_prefix, 1372 - span: Span::new(last_word_start, byte_pos), 1373 - }); 1374 - web_sys::console::log_1(&JsValue::from_str(&format!( 1375 - "[completion] Set Variable context with prefix: {:?}", 1376 - var_prefix_clone 1377 - ))); 1378 - } 1379 - } else { 1380 - // Simple variable completion 1381 - let var_prefix = trimmed_word[1..].to_string(); 1382 - let var_prefix_clone = var_prefix.clone(); 1383 - context = Some(CompletionContext::Variable { 1384 - prefix: var_prefix, 1385 - span: Span::new(last_word_start, byte_pos), 1386 - }); 1387 - web_sys::console::log_1(&JsValue::from_str(&format!( 1388 - "[completion] Set Variable context with prefix: {:?}", 1389 - var_prefix_clone 1390 - ))); 1391 - } 1392 - } else if trimmed_word.starts_with('-') { 1393 - // Try to find command by looking backwards through shapes 1394 - let mut found_cmd = None; 1395 - for (span, shape) in shapes.iter().rev() { 1396 - let local_span = to_local_span(*span); 1397 - if local_span.end <= byte_pos && is_command_shape(shape, local_span) { 1398 - let cmd_text = safe_slice(local_span); 1399 - let cmd_name = cmd_text 1400 - .split_whitespace() 1401 - .next() 1402 - .unwrap_or(&cmd_text) 1403 - .trim(); 1404 - found_cmd = Some(cmd_name.to_string()); 1405 - break; 1406 - } 1407 - } 1408 - if let Some(cmd_name) = found_cmd { 1409 - let cmd_name_clone = cmd_name.clone(); 1410 - context = Some(CompletionContext::Flag { 1411 - prefix: trimmed_word.to_string(), 1412 - span: Span::new(last_word_start, byte_pos), 1413 - command_name: cmd_name, 1414 - }); 1415 - web_sys::console::log_1(&JsValue::from_str(&format!( 1416 - "[completion] Set Flag context with prefix: {:?}, command: {:?}", 1417 - trimmed_word, cmd_name_clone 1418 - ))); 1419 - } else { 1420 - context = Some(CompletionContext::Argument { 1421 - prefix: last_word.to_string(), 1422 - span: Span::new(last_word_start, byte_pos), 1423 - }); 1424 - web_sys::console::log_1(&JsValue::from_str(&format!( 1425 - "[completion] Set Argument context with prefix: {:?}", 1426 - last_word 1427 - ))); 1428 - } 1429 - } else { 1430 - // Try to find command and argument index 1431 - let mut found_cmd = None; 1432 - let mut arg_count = 0; 1433 - for (span, shape) in shapes.iter().rev() { 1434 - let local_span = to_local_span(*span); 1435 - if local_span.end <= byte_pos { 1436 - if is_command_shape(shape, local_span) { 1437 - let cmd_text = safe_slice(local_span); 1438 - let cmd_name = cmd_text 1439 - .split_whitespace() 1440 - .next() 1441 - .unwrap_or(&cmd_text) 1442 - .trim(); 1443 - found_cmd = Some(cmd_name.to_string()); 1444 - break; 1445 - } else { 1446 - let arg_text = safe_slice(local_span); 1447 - let trimmed_arg = arg_text.trim(); 1448 - if !trimmed_arg.is_empty() && !trimmed_arg.starts_with('-') { 1449 - arg_count += 1; 1450 - } 1451 - } 1452 - } 1453 - } 1454 - if let Some(cmd_name) = found_cmd { 1455 - let cmd_name_clone = cmd_name.clone(); 1456 - context = Some(CompletionContext::CommandArgument { 1457 - prefix: trimmed_word.to_string(), 1458 - span: Span::new(last_word_start, byte_pos), 1459 - command_name: cmd_name, 1460 - arg_index: arg_count, 1461 - }); 1462 - web_sys::console::log_1(&JsValue::from_str(&format!( 1463 - "[completion] Set CommandArgument context with prefix: {:?}, command: {:?}, arg_index: {}", 1464 - trimmed_word, cmd_name_clone, arg_count 1465 - ))); 1466 - } else { 1467 - context = Some(CompletionContext::Argument { 1468 - prefix: last_word.to_string(), 1469 - span: Span::new(last_word_start, byte_pos), 1470 - }); 1471 - web_sys::console::log_1(&JsValue::from_str(&format!( 1472 - "[completion] Set Argument context with prefix: {:?}", 1473 - last_word 1474 - ))); 1475 - } 1476 - } 1477 - } 1478 - } 1479 - } 1480 - 1481 - web_sys::console::log_1(&JsValue::from_str(&format!("context: {:?}", context))); 1482 - 1483 - let mut suggestions: Vec<Suggestion> = Vec::new(); 1484 - 1485 - // Convert byte-spans back to char-spans for JS 1486 - let to_char_span = |span: Span| -> Span { 1487 - let char_start = input[..span.start].chars().count(); 1488 - let char_end = input[..span.end].chars().count(); 1489 - Span::new(char_start, char_end) 1490 - }; 1491 - 1492 - match context { 1493 - Some(CompletionContext::Command { prefix, span }) => { 1494 - web_sys::console::log_1(&JsValue::from_str(&format!( 1495 - "[completion] Generating Command suggestions with prefix: {:?}", 1496 - prefix 1497 - ))); 1498 - // Command completion 1499 - let cmds = working_set 1500 - .find_commands_by_predicate(|value| value.starts_with(prefix.as_bytes()), true); 1501 - 1502 - let span = to_char_span(span); 1503 - let mut cmd_count = 0; 1504 - 1505 - for (_, name, desc, _) in cmds { 1506 - let name_str = String::from_utf8_lossy(&name).to_string(); 1507 - suggestions.push(Suggestion { 1508 - rendered: { 1509 - let name_colored = ansi_term::Color::Green.bold().paint(&name_str); 1510 - let desc_str = desc.as_deref().unwrap_or("<no description>"); 1511 - format!("{name_colored} {desc_str}") 1512 - }, 1513 - name: name_str, 1514 - description: desc, 1515 - is_command: true, 1516 - span_start: span.start, 1517 - span_end: span.end, 1518 - }); 1519 - cmd_count += 1; 1520 - } 1521 - web_sys::console::log_1(&JsValue::from_str(&format!( 1522 - "[completion] Found {} command suggestions", 1523 - cmd_count 1524 - ))); 1525 - } 1526 - Some(CompletionContext::Argument { prefix, span }) => { 1527 - web_sys::console::log_1(&JsValue::from_str(&format!( 1528 - "[completion] Generating Argument suggestions with prefix: {:?}", 1529 - prefix 1530 - ))); 1531 - // File completion 1532 - let (dir, file_prefix) = prefix 1533 - .rfind('/') 1534 - .map(|idx| (&prefix[..idx + 1], &prefix[idx + 1..])) 1535 - .unwrap_or(("", prefix.as_str())); 1536 - 1537 - let dir_to_join = (dir.len() > 1 && dir.ends_with('/')) 1538 - .then(|| &dir[..dir.len() - 1]) 1539 - .unwrap_or(dir); 1540 - 1541 - let target_dir = if !dir.is_empty() { 1542 - match root.join(dir_to_join) { 1543 - Ok(d) if d.is_dir().unwrap_or(false) => Some(d), 1544 - _ => None, 1545 - } 1546 - } else { 1547 - Some(root.join("").unwrap()) 1548 - }; 1549 - 1550 - let mut file_count = 0; 1551 - if let Some(d) = target_dir { 1552 - if let Ok(iterator) = d.read_dir() { 1553 - let span = to_char_span(span); 1554 - 1555 - for entry in iterator { 1556 - let name = entry.filename(); 1557 - if name.starts_with(file_prefix) { 1558 - let full_completion = format!("{}{}", dir, name); 1559 - suggestions.push(Suggestion { 1560 - name: full_completion.clone(), 1561 - description: None, 1562 - is_command: false, 1563 - rendered: full_completion, 1564 - span_start: span.start, 1565 - span_end: span.end, 1566 - }); 1567 - file_count += 1; 1568 - } 1569 - } 1570 - } 1571 - } 1572 - web_sys::console::log_1(&JsValue::from_str(&format!( 1573 - "[completion] Found {} file suggestions", 1574 - file_count 1575 - ))); 1576 - } 1577 - Some(CompletionContext::Flag { 1578 - prefix, 1579 - span, 1580 - command_name, 1581 - }) => { 1582 - web_sys::console::log_1(&JsValue::from_str(&format!( 1583 - "[completion] Generating Flag suggestions for command: {:?}, prefix: {:?}", 1584 - command_name, prefix 1585 - ))); 1586 - 1587 - if let Some(signature) = get_command_signature(&command_name) { 1588 - let span = to_char_span(span); 1589 - let mut flag_count = 0; 1590 - 1591 - // Get switches from signature 1592 - // Signature has a named field that contains named arguments (including switches) 1593 - for flag in &signature.named { 1594 - // Check if this is a switch (has no argument) 1595 - // Switches have arg: None, named arguments have arg: Some(SyntaxShape) 1596 - let is_switch = flag.arg.is_none(); 1597 - 1598 - if is_switch { 1599 - let long_name = format!("--{}", flag.long); 1600 - let short_name = flag.short.map(|c| format!("-{}", c)); 1601 - 1602 - // Determine which flags to show based on prefix: 1603 - // - If prefix is empty or exactly "-", show all flags (both short and long) 1604 - // - If prefix starts with "--", only show long flags that match the prefix 1605 - // - If prefix starts with "-" (but not "--"), only show short flags that match the prefix 1606 - let show_all = prefix.is_empty() || prefix == "-"; 1607 - 1608 - // Helper to create a flag suggestion 1609 - let create_flag_suggestion = |flag_name: String| -> Suggestion { 1610 - Suggestion { 1611 - name: flag_name.clone(), 1612 - description: Some(flag.desc.clone()), 1613 - is_command: false, 1614 - rendered: { 1615 - let flag_colored = 1616 - ansi_term::Color::Cyan.bold().paint(&flag_name); 1617 - format!("{flag_colored} {}", flag.desc) 1618 - }, 1619 - span_start: span.start, 1620 - span_end: span.end, 1621 - } 1622 - }; 1623 - 1624 - // Add long flag if it matches 1625 - let should_show_long = if show_all { 1626 - true // Show all flags when prefix is "-" or empty 1627 - } else if prefix.starts_with("--") { 1628 - long_name.starts_with(&prefix) // Only show long flags matching prefix 1629 - } else { 1630 - false // Don't show long flags if prefix is short flag format 1631 - }; 1632 - 1633 - if should_show_long { 1634 - suggestions.push(create_flag_suggestion(long_name)); 1635 - flag_count += 1; 1636 - } 1637 - 1638 - // Add short flag if it matches 1639 - if let Some(short) = &short_name { 1640 - let should_show_short = if show_all { 1641 - true // Show all flags when prefix is "-" or empty 1642 - } else if prefix.starts_with("-") && !prefix.starts_with("--") { 1643 - short.starts_with(&prefix) // Only show short flags matching prefix 1644 - } else { 1645 - false // Don't show short flags if prefix is long flag format 1646 - }; 1647 - 1648 - if should_show_short { 1649 - suggestions.push(create_flag_suggestion(short.clone())); 1650 - flag_count += 1; 1651 - } 1652 - } 1653 - } 1654 - } 1655 - 1656 - web_sys::console::log_1(&JsValue::from_str(&format!( 1657 - "[completion] Found {} flag suggestions", 1658 - flag_count 1659 - ))); 1660 - } else { 1661 - web_sys::console::log_1(&JsValue::from_str(&format!( 1662 - "[completion] Could not find signature for command: {:?}", 1663 - command_name 1664 - ))); 1665 - } 1666 - } 1667 - Some(CompletionContext::CommandArgument { 1668 - prefix, 1669 - span, 1670 - command_name, 1671 - arg_index, 1672 - }) => { 1673 - web_sys::console::log_1(&JsValue::from_str(&format!( 1674 - "[completion] Generating CommandArgument suggestions for command: {:?}, arg_index: {}, prefix: {:?}", 1675 - command_name, arg_index, prefix 1676 - ))); 1677 - 1678 - if let Some(signature) = get_command_signature(&command_name) { 1679 - // Get positional arguments from signature 1680 - // Combine required and optional positional arguments 1681 - let mut all_positional = Vec::new(); 1682 - all_positional.extend_from_slice(&signature.required_positional); 1683 - all_positional.extend_from_slice(&signature.optional_positional); 1684 - 1685 - // Find the argument at the given index 1686 - if let Some(arg) = all_positional.get(arg_index) { 1687 - // Check the SyntaxShape to determine completion type 1688 - match &arg.shape { 1689 - nu_protocol::SyntaxShape::String | nu_protocol::SyntaxShape::Filepath => { 1690 - // File/directory completion 1691 - let (dir, file_prefix) = prefix 1692 - .rfind('/') 1693 - .map(|idx| (&prefix[..idx + 1], &prefix[idx + 1..])) 1694 - .unwrap_or(("", prefix.as_str())); 1695 - 1696 - let dir_to_join = (dir.len() > 1 && dir.ends_with('/')) 1697 - .then(|| &dir[..dir.len() - 1]) 1698 - .unwrap_or(dir); 1699 - 1700 - let target_dir = if !dir.is_empty() { 1701 - match root.join(dir_to_join) { 1702 - Ok(d) if d.is_dir().unwrap_or(false) => Some(d), 1703 - _ => None, 1704 - } 1705 - } else { 1706 - Some(root.join("").unwrap()) 1707 - }; 1708 - 1709 - let span = to_char_span(span); 1710 - let mut file_count = 0; 1711 - if let Some(d) = target_dir { 1712 - if let Ok(iterator) = d.read_dir() { 1713 - for entry in iterator { 1714 - let name = entry.filename(); 1715 - if name.starts_with(file_prefix) { 1716 - let full_completion = format!("{}{}", dir, name); 1717 - suggestions.push(Suggestion { 1718 - name: full_completion.clone(), 1719 - description: Some(arg.desc.clone()), 1720 - is_command: false, 1721 - rendered: full_completion, 1722 - span_start: span.start, 1723 - span_end: span.end, 1724 - }); 1725 - file_count += 1; 1726 - } 1727 - } 1728 - } 1729 - } 1730 - web_sys::console::log_1(&JsValue::from_str(&format!( 1731 - "[completion] Found {} file suggestions for argument {}", 1732 - file_count, arg_index 1733 - ))); 1734 - } 1735 - _ => { 1736 - // For other types, fall back to file completion 1737 - let (dir, file_prefix) = prefix 1738 - .rfind('/') 1739 - .map(|idx| (&prefix[..idx + 1], &prefix[idx + 1..])) 1740 - .unwrap_or(("", prefix.as_str())); 1741 - 1742 - let dir_to_join = (dir.len() > 1 && dir.ends_with('/')) 1743 - .then(|| &dir[..dir.len() - 1]) 1744 - .unwrap_or(dir); 1745 - 1746 - let target_dir = if !dir.is_empty() { 1747 - match root.join(dir_to_join) { 1748 - Ok(d) if d.is_dir().unwrap_or(false) => Some(d), 1749 - _ => None, 1750 - } 1751 - } else { 1752 - Some(root.join("").unwrap()) 1753 - }; 1754 - 1755 - let span = to_char_span(span); 1756 - if let Some(d) = target_dir { 1757 - if let Ok(iterator) = d.read_dir() { 1758 - for entry in iterator { 1759 - let name = entry.filename(); 1760 - if name.starts_with(file_prefix) { 1761 - let full_completion = format!("{}{}", dir, name); 1762 - suggestions.push(Suggestion { 1763 - name: full_completion.clone(), 1764 - description: Some(arg.desc.clone()), 1765 - is_command: false, 1766 - rendered: full_completion, 1767 - span_start: span.start, 1768 - span_end: span.end, 1769 - }); 1770 - } 1771 - } 1772 - } 1773 - } 1774 - } 1775 - } 1776 - } else { 1777 - // Argument index out of range, fall back to file completion 1778 - web_sys::console::log_1(&JsValue::from_str(&format!( 1779 - "[completion] Argument index {} out of range, using file completion", 1780 - arg_index 1781 - ))); 1782 - // Use the same file completion logic as Argument context 1783 - let (dir, file_prefix) = prefix 1784 - .rfind('/') 1785 - .map(|idx| (&prefix[..idx + 1], &prefix[idx + 1..])) 1786 - .unwrap_or(("", prefix.as_str())); 1787 - 1788 - let dir_to_join = (dir.len() > 1 && dir.ends_with('/')) 1789 - .then(|| &dir[..dir.len() - 1]) 1790 - .unwrap_or(dir); 1791 - 1792 - let target_dir = if !dir.is_empty() { 1793 - match root.join(dir_to_join) { 1794 - Ok(d) if d.is_dir().unwrap_or(false) => Some(d), 1795 - _ => None, 1796 - } 1797 - } else { 1798 - Some(root.join("").unwrap()) 1799 - }; 1800 - 1801 - let span = to_char_span(span); 1802 - if let Some(d) = target_dir { 1803 - if let Ok(iterator) = d.read_dir() { 1804 - for entry in iterator { 1805 - let name = entry.filename(); 1806 - if name.starts_with(file_prefix) { 1807 - let full_completion = format!("{}{}", dir, name); 1808 - suggestions.push(Suggestion { 1809 - name: full_completion.clone(), 1810 - description: None, 1811 - is_command: false, 1812 - rendered: full_completion, 1813 - span_start: span.start, 1814 - span_end: span.end, 1815 - }); 1816 - } 1817 - } 1818 - } 1819 - } 1820 - } 1821 - } else { 1822 - // No signature found, fall back to file completion 1823 - web_sys::console::log_1(&JsValue::from_str(&format!( 1824 - "[completion] Could not find signature for command: {:?}, using file completion", 1825 - command_name 1826 - ))); 1827 - let (dir, file_prefix) = prefix 1828 - .rfind('/') 1829 - .map(|idx| (&prefix[..idx + 1], &prefix[idx + 1..])) 1830 - .unwrap_or(("", prefix.as_str())); 1831 - 1832 - let dir_to_join = (dir.len() > 1 && dir.ends_with('/')) 1833 - .then(|| &dir[..dir.len() - 1]) 1834 - .unwrap_or(dir); 1835 - 1836 - let target_dir = if !dir.is_empty() { 1837 - match root.join(dir_to_join) { 1838 - Ok(d) if d.is_dir().unwrap_or(false) => Some(d), 1839 - _ => None, 1840 - } 1841 - } else { 1842 - Some(root.join("").unwrap()) 1843 - }; 1844 - 1845 - let span = to_char_span(span); 1846 - if let Some(d) = target_dir { 1847 - if let Ok(iterator) = d.read_dir() { 1848 - for entry in iterator { 1849 - let name = entry.filename(); 1850 - if name.starts_with(file_prefix) { 1851 - let full_completion = format!("{}{}", dir, name); 1852 - suggestions.push(Suggestion { 1853 - name: full_completion.clone(), 1854 - description: None, 1855 - is_command: false, 1856 - rendered: full_completion, 1857 - span_start: span.start, 1858 - span_end: span.end, 1859 - }); 1860 - } 1861 - } 1862 - } 1863 - } 1864 - } 1865 - } 1866 - Some(CompletionContext::Variable { prefix, span }) => { 1867 - web_sys::console::log_1(&JsValue::from_str(&format!( 1868 - "[completion] Generating Variable suggestions with prefix: {:?}", 1869 - prefix 1870 - ))); 1871 - 1872 - // Collect all available variables 1873 - let variables = collect_variables(&working_set, &input, byte_pos); 1874 - let span = to_char_span(span); 1875 - let mut var_count = 0; 1876 - 1877 - for (var_name, var_id) in variables { 1878 - // Filter by prefix (variable name includes $, so we need to check after $) 1879 - if var_name.len() > 1 && var_name[1..].starts_with(&prefix) { 1880 - // Get variable type 1881 - let var_type = working_set.get_variable(var_id).ty.to_string(); 1882 - 1883 - suggestions.push(Suggestion { 1884 - name: var_name.clone(), 1885 - description: Some(var_type.clone()), 1886 - is_command: false, 1887 - rendered: { 1888 - let var_colored = ansi_term::Color::Blue.bold().paint(&var_name); 1889 - format!("{var_colored} {var_type}") 1890 - }, 1891 - span_start: span.start, 1892 - span_end: span.end, 1893 - }); 1894 - var_count += 1; 1895 - } 1896 - } 1897 - 1898 - web_sys::console::log_1(&JsValue::from_str(&format!( 1899 - "[completion] Found {} variable suggestions", 1900 - var_count 1901 - ))); 1902 - } 1903 - Some(CompletionContext::CellPath { 1904 - prefix, 1905 - span, 1906 - var_id, 1907 - path_so_far, 1908 - }) => { 1909 - web_sys::console::log_1(&JsValue::from_str(&format!( 1910 - "[completion] Generating CellPath suggestions with prefix: {:?}, path: {:?}", 1911 - prefix, path_so_far 1912 - ))); 1913 - 1914 - // Evaluate the variable to get its value 1915 - if let Some(var_value) = eval_variable_for_completion(var_id, &working_set) { 1916 - // Follow the path to get the value at the current level 1917 - let current_value = if path_so_far.is_empty() { 1918 - var_value 1919 - } else { 1920 - follow_cell_path(&var_value, &path_so_far).unwrap_or(var_value) 1921 - }; 1922 - 1923 - // Get columns/fields from the current value 1924 - let columns = get_columns_from_value(&current_value); 1925 - let span = to_char_span(span); 1926 - let mut field_count = 0; 1927 - 1928 - for (col_name, col_type) in columns { 1929 - // Filter by prefix 1930 - if col_name.starts_with(&prefix) { 1931 - let type_str = col_type.as_deref().unwrap_or("any"); 1932 - suggestions.push(Suggestion { 1933 - name: col_name.clone(), 1934 - description: Some(type_str.to_string()), 1935 - is_command: false, 1936 - rendered: { 1937 - let col_colored = ansi_term::Color::Yellow.paint(&col_name); 1938 - format!("{col_colored} {type_str}") 1939 - }, 1940 - span_start: span.start, 1941 - span_end: span.end, 1942 - }); 1943 - field_count += 1; 1944 - } 1945 - } 1946 - 1947 - web_sys::console::log_1(&JsValue::from_str(&format!( 1948 - "[completion] Found {} cell path suggestions", 1949 - field_count 1950 - ))); 1951 - } else { 1952 - // Variable couldn't be evaluated - this is expected for runtime variables 1953 - // We can't provide cell path completions without knowing the structure 1954 - web_sys::console::log_1(&JsValue::from_str(&format!( 1955 - "[completion] Could not evaluate variable {:?} for cell path completion (runtime variable)", 1956 - var_id 1957 - ))); 1958 - 1959 - // Try to get type information to provide better feedback 1960 - if let Ok(var_info) = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { 1961 - working_set.get_variable(var_id) 1962 - })) { 1963 - web_sys::console::log_1(&JsValue::from_str(&format!( 1964 - "[completion] Variable type: {:?}", 1965 - var_info.ty 1966 - ))); 1967 - } 1968 - } 1969 - } 1970 - _ => { 1971 - web_sys::console::log_1(&JsValue::from_str( 1972 - "[completion] Context is None, no suggestions generated", 1973 - )); 1974 - } 1975 - } 1976 - 1977 - drop(working_set); 1978 - drop(engine_guard); 1979 - 1980 - suggestions.sort(); 1981 - let suggestions = serde_json::to_string(&suggestions).unwrap_or_else(|_| "[]".to_string()); 1982 - web_sys::console::log_1(&JsValue::from_str(&suggestions)); 1983 - suggestions 1984 - }
+1 -1
src/default_context.rs
··· 264 264 bind_command! { 265 265 ExportEnv, 266 266 LoadEnv, 267 - SourceEnv, 267 + // SourceEnv, 268 268 WithEnv, 269 269 ConfigNu, 270 270 ConfigEnv,
+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
··· 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
··· 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
··· 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 { 2 - "name": "dysnomia", 2 + "name": "faunu", 3 3 "private": true, 4 4 "version": "0.0.0", 5 5 "type": "module",
www/public/Web437_EverexME_5x8.woff

This is a binary file and will not be displayed.

+33 -24
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", ··· 300 304 completionCandidates = matches.map((cmd) => ({ 301 305 name: cmd, 302 306 description: "", 303 - is_command: true, 304 307 rendered: cmd, 305 308 span_start: 0, 306 309 span_end: currentLine.length, ··· 315 318 } 316 319 }; 317 320 318 - // 1. Setup Input Handler immediately so terminal isn't blocked 319 321 term.onData(async (e) => { 320 322 // SEARCH MODE INPUT 321 323 if (searchState !== "none" && e >= " " && e <= "~") { ··· 406 408 "\x1b[33mengine is still loading, please wait (_ _ )zZ...\x1b[0m\r\n", 407 409 ); 408 410 } else { 409 - // ASYNC execution 410 - const output: string | undefined = await callWorker( 411 - "run", 412 - trimmed, 413 - ); 414 - if (output) { 415 - term.write(output.replace(/\n/g, "\r\n")); 416 - if (output && !output.endsWith("\n")) { 417 - 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 + } 418 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")); 419 430 } 420 431 421 - // Update cached PWD after command execution (cd, etc) 432 + // update pwd 422 433 cachedPwd = await callWorker("get_pwd"); 423 - 424 - // Update History 425 - const idx = history.indexOf(trimmed); 426 - if (idx >= 0) history.splice(idx, 1); 427 - history.push(trimmed); 428 - historyIndex = history.length; 429 434 } 430 435 } catch (err) { 431 - 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 + ); 432 439 } finally { 433 440 isRunningCommand = false; 434 441 } ··· 649 656 await readyPromise; 650 657 651 658 await callWorker("set-interrupt-buffer", interruptBuffer); 652 - await callWorker("run", "open welcome.txt"); 653 659 654 660 term.write(getPrompt()); 655 661 ··· 662 668 .catch((e) => { 663 669 term.write(`\r\n\x1b[31mfatal: failed to load engine: ${e}\x1b[0m\r\n`); 664 670 }); 671 + 672 + fitAddon.fit(); 673 + // setTimeout(() => fitAddon.fit(), 1000); 665 674 } 666 675 667 676 bootstrap().catch(console.error);
+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
··· 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) {