diff --git a/Cargo.lock b/Cargo.lock index 539479edfd..caba5ede70 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5023,7 +5023,6 @@ dependencies = [ "sysinfo", "tabled", "tempfile", - "titlecase", "toml", "toml_edit", "trash", @@ -9448,15 +9447,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" -[[package]] -name = "titlecase" -version = "3.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb567088a91d59b492520c8149e2be5ce10d5deb2d9a383f3378df3259679d40" -dependencies = [ - "regex", -] - [[package]] name = "tokio" version = "1.53.1" diff --git a/Cargo.toml b/Cargo.toml index 09f54829f8..9a155099e0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -271,7 +271,6 @@ sysinfo = "0.39.6" tabled = { version = "0.22", default-features = false } tempfile = "3.27" thiserror = "2.0.20" -titlecase = "3.6" tokio = { version = "1.53.1" } toml = "1.1.4" toml_edit = "0.25.13" diff --git a/crates/nu-cli/src/commands/commandline/set_prompt.rs b/crates/nu-cli/src/commands/commandline/set_prompt.rs index c71fba356c..55fccf7e6d 100644 --- a/crates/nu-cli/src/commands/commandline/set_prompt.rs +++ b/crates/nu-cli/src/commands/commandline/set_prompt.rs @@ -1,5 +1,5 @@ use nu_engine::command_prelude::*; -use nu_protocol::engine::PromptSegment; +use nu_protocol::engine::PromptContents; #[derive(Clone)] pub struct CommandlineSetPrompt; @@ -66,7 +66,11 @@ segments that have finished computing. The line and cursor are preserved. `--indicator` sets only the default/emacs indicator. Use `--vi-insert` and `--vi-normal` to set the vi mode indicators independently. -The pushed prompt lasts only until the next prompt is drawn. +The pushed prompt lasts only until the next prompt is drawn. The exception is a +push from code that runs while the next prompt is built (a `pre_prompt` or +`env_change` hook, or a prompt closure such as `$env.PROMPT_COMMAND`), or from a +job that code starts: it shows on that prompt even if it lands before the prompt +is drawn. meant for REPL sessions only"# } @@ -110,26 +114,18 @@ meant for REPL sessions only"# || vi_normal.is_some() || multiline.is_some() { - engine_state.prompt_state.apply(|contents| { - if let Some(content) = left { - contents.apply_segment_override(PromptSegment::Left, content); - } - if let Some(content) = right { - contents.apply_segment_override(PromptSegment::Right, content); - } - if let Some(content) = indicator { - contents.apply_segment_override(PromptSegment::Indicator, content); - } - if let Some(content) = vi_insert { - contents.apply_segment_override(PromptSegment::ViInsert, content); - } - if let Some(content) = vi_normal { - contents.apply_segment_override(PromptSegment::ViNormal, content); - } - if let Some(content) = multiline { - contents.apply_segment_override(PromptSegment::Multiline, content); - } - }); + let pushed = PromptContents { + left: left.map(Into::into), + right: right.map(Into::into), + indicator: indicator.map(Into::into), + vi_insert: vi_insert.map(Into::into), + vi_normal: vi_normal.map(Into::into), + multiline: multiline.map(Into::into), + ..Default::default() + }; + engine_state + .prompt_state + .apply(pushed, engine_state.current_job.root); } Ok(Value::nothing(head).into_pipeline_data()) diff --git a/crates/nu-cli/src/prompt_update.rs b/crates/nu-cli/src/prompt_update.rs index eb62c89ddb..f284f236b4 100644 --- a/crates/nu-cli/src/prompt_update.rs +++ b/crates/nu-cli/src/prompt_update.rs @@ -92,7 +92,8 @@ fn get_prompt_string( /// Re-evaluate `$env.PROMPT_COMMAND` and friends and install the result as the /// prompt's per-cycle baseline. This overwrites anything a background job pushed -/// during the previous cycle, resetting the prompt for the next line. +/// during the previous cycle, resetting the prompt for the next line; the pushes +/// this cycle's `PromptState::begin_update` kept stay on top. pub fn update_prompt(config: &Config, engine_state: &EngineState, stack: &mut Stack) { let new_contents = build_prompt_contents(config, engine_state, stack); @@ -181,7 +182,7 @@ mod tests { #[test] fn transient_prompt_override_still_wins_over_the_live_baseline() { - use nu_protocol::engine::PromptSegment; + use nu_protocol::{JobId, engine::PromptSegment}; let config = Config::default(); let engine_state = EngineState::new(); @@ -196,7 +197,7 @@ mod tests { // The configured TRANSIENT_PROMPT_INDICATOR beats a later live change. engine_state .prompt_state - .set(PromptSegment::Indicator, "live> "); + .set(PromptSegment::Indicator, "live> ", JobId::new(1)); assert_eq!( transient_prompt.render_prompt_indicator(reedline::PromptEditMode::Emacs), diff --git a/crates/nu-cli/src/repl.rs b/crates/nu-cli/src/repl.rs index d19e88e018..5cf38bcf92 100644 --- a/crates/nu-cli/src/repl.rs +++ b/crates/nu-cli/src/repl.rs @@ -634,6 +634,15 @@ fn loop_iteration(ctx: LoopContext) -> (bool, Stack, Reedline) { start_time = Instant::now(); + // Keep the pushes made for this prompt even if they land before `update_prompt` installs the + // new baseline; `PromptState::begin_update` says which pushes count. + let first_job = engine_state + .jobs + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()) + .next_id(); + engine_state.prompt_state.begin_update(first_job); + // Juhan said to do this :) let mut repl = engine_state.repl_state.lock().expect("repl state mutex"); repl.cursor_pos = line_editor.current_insertion_point(); @@ -812,8 +821,8 @@ fn loop_iteration(ctx: LoopContext) -> (bool, Stack, Reedline) { start_time = Instant::now(); let config = &engine_state.get_config().clone(); - // Re-evaluate the prompt into the shared `prompt_state`; this also resets any - // segment a background job pushed during the previous cycle. + // Re-evaluate the prompt into the shared `prompt_state`, replacing the segments pushed for + // the previous prompt but not the pushes `begin_update` above kept. prompt_update::update_prompt( config, engine_state, diff --git a/crates/nu-cli/tests/commands/commandline_set_prompt.rs b/crates/nu-cli/tests/commands/commandline_set_prompt.rs new file mode 100644 index 0000000000..aca318958b --- /dev/null +++ b/crates/nu-cli/tests/commands/commandline_set_prompt.rs @@ -0,0 +1,55 @@ +use nu_cli::update_prompt; +use nu_test_support::prelude::*; + +// Each test opens the window the REPL opens before its hooks run (`PromptState::begin_update`), +// runs what a hook would, then installs the prompt baseline the way the REPL does. Jobs report +// back with `job send 0`, so the tests wait for them without sleeping. + +#[test] +fn push_from_a_job_started_for_the_prompt_survives_the_baseline() -> Result { + let mut tester = test().env("PROMPT_COMMAND", "baseline> "); + let first_job = tester + .engine_state + .jobs + .lock() + .expect("jobs lock") + .next_id(); + tester.engine_state.prompt_state.begin_update(first_job); + + let () = tester.run( + "job spawn { commandline set-prompt 'pushed> '; 'done' | job send 0 }; job recv --timeout 1min | ignore", + )?; + let config = tester.engine_state.get_config().clone(); + update_prompt(&config, &tester.engine_state, &mut tester.stack); + + let left = tester.engine_state.prompt_state.contents().left; + assert_eq!(left.as_deref(), Some("pushed> ")); + Ok(()) +} + +#[test] +fn push_from_a_job_spawned_by_an_earlier_job_does_not_survive_the_baseline() -> Result { + let mut tester = test().env("PROMPT_COMMAND", "baseline> "); + // A job left over from an earlier prompt: it spawns the job that pushes once told to. + let earlier: i64 = tester.run( + "job spawn { job recv | ignore; job spawn { commandline set-prompt 'stale> '; 'done' | job send 0 } }", + )?; + let first_job = tester + .engine_state + .jobs + .lock() + .expect("jobs lock") + .next_id(); + tester.engine_state.prompt_state.begin_update(first_job); + + let () = tester.run_with_data( + "let earlier = $in; 'go' | job send $earlier; job recv --timeout 1min | ignore", + earlier, + )?; + let config = tester.engine_state.get_config().clone(); + update_prompt(&config, &tester.engine_state, &mut tester.stack); + + let left = tester.engine_state.prompt_state.contents().left; + assert_eq!(left.as_deref(), Some("baseline> ")); + Ok(()) +} diff --git a/crates/nu-cli/tests/commands/mod.rs b/crates/nu-cli/tests/commands/mod.rs index af1ab58d97..2c1cd1e579 100644 --- a/crates/nu-cli/tests/commands/mod.rs +++ b/crates/nu-cli/tests/commands/mod.rs @@ -1,3 +1,4 @@ +mod commandline_set_prompt; mod keybindings_list; mod nu_highlight; diff --git a/crates/nu-command/Cargo.toml b/crates/nu-command/Cargo.toml index 69d783590e..01b5cfa42e 100644 --- a/crates/nu-command/Cargo.toml +++ b/crates/nu-command/Cargo.toml @@ -123,7 +123,6 @@ sha2 = { workspace = true } strum = { workspace = true } sysinfo = { workspace = true } tabled = { workspace = true, features = ["ansi"], default-features = false } -titlecase = { workspace = true } toml = { workspace = true, features = ["preserve_order"] } toml_edit = { workspace = true } kdl = { workspace = true } diff --git a/crates/nu-command/src/date/parser.rs b/crates/nu-command/src/date/parser.rs index ad6b953396..a109dd3cf6 100644 --- a/crates/nu-command/src/date/parser.rs +++ b/crates/nu-command/src/date/parser.rs @@ -2,7 +2,6 @@ use chrono::{DateTime, FixedOffset, Local, Offset, TimeZone}; use chrono_tz::Tz; -use titlecase::titlecase; #[derive(Debug, Clone, PartialEq, Eq, Copy)] pub enum ParseErrorKind { @@ -38,16 +37,10 @@ pub fn datetime_in_timezone( } } +/// Look up an IANA time zone name, ignoring case (`utc`, `america/new_york`) and surrounding +/// whitespace, such as the newline that ends `/etc/timezone`. fn parse_timezone_internal(s: &str) -> Result { - if let Ok(tz) = s.parse() { - Ok(tz) - } else if let Ok(tz) = titlecase(s).parse() { - Ok(tz) - } else if let Ok(tz) = s.to_uppercase().parse() { - Ok(tz) - } else { - Err(ParseErrorKind::Invalid) - } + Tz::from_str_insensitive(s.trim()).map_err(|_| ParseErrorKind::Invalid) } fn timezone_offset_internal( diff --git a/crates/nu-command/src/experimental/job_spawn.rs b/crates/nu-command/src/experimental/job_spawn.rs index 00aafdce27..c40e9ef37f 100644 --- a/crates/nu-command/src/experimental/job_spawn.rs +++ b/crates/nu-command/src/experimental/job_spawn.rs @@ -85,6 +85,11 @@ impl Command for JobSpawn { job_state.current_job = CurrentJob { id, + root: if engine_state.is_background_job() { + engine_state.current_job.root + } else { + id + }, background_thread_job: Some(thread_job), mailbox: Arc::new(Mutex::new(Mailbox::new(recv))), }; diff --git a/crates/nu-command/tests/commands/date/to_timezone.rs b/crates/nu-command/tests/commands/date/to_timezone.rs index fa2ab64458..aad8c8a3fb 100644 --- a/crates/nu-command/tests/commands/date/to_timezone.rs +++ b/crates/nu-command/tests/commands/date/to_timezone.rs @@ -1,4 +1,5 @@ use nu_test_support::prelude::*; +use rstest::rstest; #[test] fn to_timezone_list_of_strings() -> Result { @@ -24,3 +25,22 @@ fn to_timezone_single_date_string() -> Result { r#""2020-10-10 10:00:00 +02:00" | date to-timezone "+0500" | into record | get timezone"#; test().run(code).expect_value_eq("+05:00") } + +#[rstest] +#[case::lowercase_utc("utc", "+0000")] +#[case::lowercase_area_location("america/new_york", "-0500")] +#[case::lowercase_alias("us/pacific", "-0800")] +#[case::mixed_case("eUROPE/lONDON", "+0000")] +#[case::trailing_newline("America/New_York\n", "-0500")] +#[case::surrounding_spaces(" europe/london ", "+0000")] +fn to_timezone_name_ignores_case_and_whitespace( + #[case] zone: &str, + #[case] offset: &str, +) -> Result { + test() + .run_with_data( + r#"let zone = $in; 2026-01-01T12:00:00Z | date to-timezone $zone | format date "%z""#, + zone, + ) + .expect_value_eq(offset) +} diff --git a/crates/nu-protocol/src/engine/engine_state.rs b/crates/nu-protocol/src/engine/engine_state.rs index f218444af3..0137b671aa 100644 --- a/crates/nu-protocol/src/engine/engine_state.rs +++ b/crates/nu-protocol/src/engine/engine_state.rs @@ -336,6 +336,7 @@ impl EngineState { jobs: Arc::new(Mutex::new(Jobs::default())), current_job: CurrentJob { id: JobId::new(0), + root: JobId::new(0), background_thread_job: None, mailbox: Arc::new(Mutex::new(Mailbox::new(recv))), }, @@ -1278,6 +1279,7 @@ impl EngineState { self.jobs = Default::default(); self.current_job = CurrentJob { id: JobId::new(0), + root: JobId::new(0), background_thread_job: None, mailbox: Arc::new(Mutex::new(Mailbox::new(recv))), }; diff --git a/crates/nu-protocol/src/engine/jobs.rs b/crates/nu-protocol/src/engine/jobs.rs index e206ddccda..96af35bcb2 100644 --- a/crates/nu-protocol/src/engine/jobs.rs +++ b/crates/nu-protocol/src/engine/jobs.rs @@ -43,6 +43,12 @@ impl Jobs { self.jobs.iter().map(|(k, v)| (*k, v)) } + /// The id the next job added to the table will get. Ids only grow, so a job with a lower id + /// was started before this call. + pub fn next_id(&self) -> JobId { + JobId::new(self.next_job_id) + } + /// Whether there are no tracked jobs (running or frozen). pub fn is_empty(&self) -> bool { self.jobs.is_empty() @@ -71,7 +77,7 @@ impl Jobs { } pub fn add_job(&mut self, job: Job) -> JobId { - let this_id = JobId::new(self.next_job_id); + let this_id = self.next_id(); self.assign_last_frozen_id_if_frozen(this_id, &job); @@ -262,6 +268,11 @@ impl FrozenJob { pub struct CurrentJob { pub id: JobId, + /// The job the main thread spawned that this one descends from: `id` itself when the main + /// thread spawned this job, the spawning job's `root` otherwise, and `JobId::ZERO` on the + /// main thread. + pub root: JobId, + // The background thread job associated with this thread. // If None, it indicates this thread is currently the main job pub background_thread_job: Option, diff --git a/crates/nu-protocol/src/engine/prompt_state.rs b/crates/nu-protocol/src/engine/prompt_state.rs index 4f0ae9d99b..fbeb029ff8 100644 --- a/crates/nu-protocol/src/engine/prompt_state.rs +++ b/crates/nu-protocol/src/engine/prompt_state.rs @@ -1,5 +1,7 @@ use std::sync::{Arc, Mutex, MutexGuard, RwLock, RwLockReadGuard, RwLockWriteGuard}; +use crate::JobId; + /// Wakes the active interactive line editor and asks it to repaint the prompt in /// place, without disturbing the line currently being edited. type Repainter = Arc; @@ -53,11 +55,12 @@ impl PromptContents { } /// Layer `overrides` on top of `self`, preferring the override where set - /// and falling back to `self` otherwise. + /// and falling back to `self` otherwise. `render_right_on_last_line` always + /// comes from `self`, so a push never changes it. /// - /// Used to render the transient prompt from the *live* baseline (so late - /// [`PromptState::set`] pushes still show up) with `TRANSIENT_PROMPT_*` - /// values layered on top. + /// This lays pushes over the live contents and over the new baseline, and + /// renders the transient prompt from the *live* baseline (so late pushes + /// still show up) with `TRANSIENT_PROMPT_*` values layered on top. pub fn overridden_by(&self, overrides: &PromptContents) -> PromptContents { PromptContents { left: overrides.left.clone().or_else(|| self.left.clone()), @@ -83,14 +86,44 @@ impl PromptContents { } } +/// The rendered prompt, plus the pushes kept while the REPL rebuilds it. +#[derive(Debug, Default)] +struct PromptData { + contents: PromptContents, + + /// The pushes kept since [`PromptState::begin_update`]. `None` outside the window between + /// that call and [`PromptState::set_contents`], which layers them over the new baseline. + /// Without it, a job that finishes before the REPL rebuilds the prompt would lose its push. + pending: Option, +} + +/// The segments pushed for the prompt the REPL is rebuilding. +#[derive(Debug)] +struct PendingPushes { + /// The id of the first job the main thread started for this prompt. A job whose root is + /// lower is left over from an earlier prompt, so its result may be out of date (computed for + /// another directory, say). + first_job: JobId, + + pushed: PromptContents, +} + +impl PendingPushes { + /// Whether a push belongs to the prompt being rebuilt (see [`PromptState::begin_update`]). + fn accepts(&self, root_job: JobId) -> bool { + root_job == JobId::ZERO || root_job >= self.first_job + } +} + /// Shared, thread-safe home for the interactive prompt's rendered content. #[derive(derive_more::Debug, Default)] pub struct PromptState { /// Upgraded to RwLock: Enables infinite concurrent reads, locking only for mutations. - contents: RwLock, + /// The pending pushes share the lock so a push can't slip between them and the contents. + data: RwLock, - /// Kept in its own lock: it is installed/cleared by the REPL and read by a - /// background job's `set`, never together with `contents`. + /// Kept in its own lock: it is installed/cleared by the REPL and read by + /// [`PromptState::apply`], never together with `data`. #[debug(skip)] repainter: Mutex>, } @@ -100,14 +133,14 @@ impl PromptState { Self::default() } - fn acquire_read_lock(&self) -> RwLockReadGuard<'_, PromptContents> { - self.contents + fn acquire_read_lock(&self) -> RwLockReadGuard<'_, PromptData> { + self.data .read() .unwrap_or_else(|poisoned_error| poisoned_error.into_inner()) } - fn acquire_write_lock(&self) -> RwLockWriteGuard<'_, PromptContents> { - self.contents + fn acquire_write_lock(&self) -> RwLockWriteGuard<'_, PromptData> { + self.data .write() .unwrap_or_else(|poisoned_error| poisoned_error.into_inner()) } @@ -123,15 +156,7 @@ impl PromptState { &self, action: impl FnOnce(&PromptContents) -> ReturnType, ) -> ReturnType { - action(&self.acquire_read_lock()) - } - - /// Run an action with exclusive, mutable access to the current contents. - fn modify_contents( - &self, - action: impl FnOnce(&mut PromptContents) -> ReturnType, - ) -> ReturnType { - action(&mut self.acquire_write_lock()) + action(&self.acquire_read_lock().contents) } /// A snapshot of the current contents. @@ -139,22 +164,59 @@ impl PromptState { self.with_contents(PromptContents::clone) } - /// Replace all prompt content (the baseline). - pub fn set_contents(&self, new_contents: PromptContents) { - self.modify_contents(|contents| *contents = new_contents); + /// Start rebuilding the prompt. Until [`Self::set_contents`], a push is also kept aside, so the + /// new baseline doesn't overwrite it, when it comes from the main thread or from a job whose + /// `CurrentJob::root` is at least `first_job`: a job the main thread started after this call, + /// or a job one of those spawned. A push from an older job still shows until the new baseline + /// replaces it. + /// + /// The REPL calls this before the hooks that run ahead of each prompt, and passes the id the + /// next spawned job will get. Until `set_contents`, everything the main thread runs prepares + /// this prompt: the `env_change` and `pre_prompt` hooks, `color_config` and `ENV_CONVERSIONS` + /// closures, and the prompt closures themselves. + pub fn begin_update(&self, first_job: JobId) { + self.acquire_write_lock().pending = Some(PendingPushes { + first_job, + pushed: PromptContents::default(), + }); } - /// Apply a batch of overrides under a single write lock, then request one - /// in-place repaint. Callers touching several segments at once should use this as to not flash-update - pub fn apply(&self, overrides: impl FnOnce(&mut PromptContents)) { - self.modify_contents(overrides); + /// Replace all prompt content (the baseline), then lay over it the pushes kept since + /// [`Self::begin_update`]. + pub fn set_contents(&self, new_contents: PromptContents) { + let mut data = self.acquire_write_lock(); + data.contents = match data.pending.take() { + Some(pending) => new_contents.overridden_by(&pending.pushed), + None => new_contents, + }; + } + + /// Replace the segments set in `pushed` under a single write lock, then request one in-place + /// repaint. Callers touching several segments at once should use this as to not flash-update. + /// + /// `root_job` is the pushing thread's `CurrentJob::root` (`JobId::ZERO` on the main thread); + /// it decides whether the push outlives the next [`Self::set_contents`] (see + /// [`Self::begin_update`]). Segments left `None` keep their current value, and + /// `render_right_on_last_line` is ignored. + pub fn apply(&self, pushed: PromptContents, root_job: JobId) { + let mut data = self.acquire_write_lock(); + data.contents = data.contents.overridden_by(&pushed); + if let Some(pending) = &mut data.pending + && pending.accepts(root_job) + { + pending.pushed = pending.pushed.overridden_by(&pushed); + } + // Release the lock before repainting: the line editor reads the prompt to redraw it. + drop(data); self.request_repaint(); } - /// Push an override for a single segment and request an in-place repaint. - pub fn set(&self, segment: PromptSegment, content: impl Into>) { - let content = content.into(); - self.apply(|contents| contents.apply_segment_override(segment, content)); + /// Push an override for a single segment and request an in-place repaint. `root_job` is as + /// in [`Self::apply`]. + pub fn set(&self, segment: PromptSegment, content: impl Into>, root_job: JobId) { + let mut pushed = PromptContents::default(); + pushed.apply_segment_override(segment, content); + self.apply(pushed, root_job); } /// Install or remove the line editor's repainter mechanism. @@ -179,6 +241,9 @@ mod tests { use super::*; use std::sync::atomic::{AtomicUsize, Ordering}; + /// A background job id, for tests where the exact id doesn't matter. + const JOB: JobId = JobId::new(1); + /// A `PromptState` wired to a repainter that counts how often it fires. fn setup_state_with_counter() -> (Arc, Arc) { let state = Arc::new(PromptState::new()); @@ -195,7 +260,7 @@ mod tests { #[test] fn set_writes_only_the_targeted_segment() { let state = PromptState::new(); - state.set(PromptSegment::Left, "LeftSegment"); + state.set(PromptSegment::Left, "LeftSegment", JOB); let contents = state.contents(); assert_eq!(contents.left.as_deref(), Some("LeftSegment")); @@ -205,12 +270,12 @@ mod tests { #[test] fn indicator_vi_insert_vi_normal_and_multiline_are_independent() { let state = PromptState::new(); - state.apply(|contents| { - contents.apply_segment_override(PromptSegment::Indicator, "Indicator"); - contents.apply_segment_override(PromptSegment::ViInsert, "ViInsert"); - contents.apply_segment_override(PromptSegment::ViNormal, "ViNormal"); - contents.apply_segment_override(PromptSegment::Multiline, "Multiline"); - }); + let mut pushed = PromptContents::default(); + pushed.apply_segment_override(PromptSegment::Indicator, "Indicator"); + pushed.apply_segment_override(PromptSegment::ViInsert, "ViInsert"); + pushed.apply_segment_override(PromptSegment::ViNormal, "ViNormal"); + pushed.apply_segment_override(PromptSegment::Multiline, "Multiline"); + state.apply(pushed, JOB); let contents = state.contents(); assert_eq!(contents.indicator.as_deref(), Some("Indicator")); @@ -222,8 +287,8 @@ mod tests { #[test] fn each_set_triggers_exactly_one_repaint() { let (state, repainter_count) = setup_state_with_counter(); - state.set(PromptSegment::Left, "Alpha"); - state.set(PromptSegment::Right, "Beta"); + state.set(PromptSegment::Left, "Alpha", JOB); + state.set(PromptSegment::Right, "Beta", JOB); assert_eq!(repainter_count.load(Ordering::Relaxed), 2); } @@ -231,11 +296,11 @@ mod tests { #[test] fn apply_batches_multiple_segments_into_one_repaint() { let (state, repainter_count) = setup_state_with_counter(); - state.apply(|contents| { - contents.apply_segment_override(PromptSegment::Left, "Alpha"); - contents.apply_segment_override(PromptSegment::Right, "Beta"); - contents.apply_segment_override(PromptSegment::Indicator, "Gamma"); - }); + let mut pushed = PromptContents::default(); + pushed.apply_segment_override(PromptSegment::Left, "Alpha"); + pushed.apply_segment_override(PromptSegment::Right, "Beta"); + pushed.apply_segment_override(PromptSegment::Indicator, "Gamma"); + state.apply(pushed, JOB); let contents = state.contents(); assert_eq!(contents.left.as_deref(), Some("Alpha")); @@ -248,7 +313,7 @@ mod tests { #[test] fn set_contents_overwrites_a_pushed_override() { let state = PromptState::new(); - state.set(PromptSegment::Left, "Pushed"); + state.set(PromptSegment::Left, "Pushed", JOB); state.set_contents(PromptContents { left: Some("Baseline".into()), @@ -258,11 +323,92 @@ mod tests { assert_eq!(state.contents().left.as_deref(), Some("Baseline")); } + #[test] + fn push_during_update_survives_set_contents() { + let state = PromptState::new(); + state.begin_update(JOB); + state.set(PromptSegment::Right, "Pushed", JOB); + + state.set_contents(PromptContents { + left: Some("BaselineLeft".into()), + right: Some("BaselineRight".into()), + ..Default::default() + }); + + let contents = state.contents(); + assert_eq!(contents.left.as_deref(), Some("BaselineLeft")); + assert_eq!(contents.right.as_deref(), Some("Pushed")); + } + + #[test] + fn begin_update_drops_pushes_from_the_previous_prompt() { + let state = PromptState::new(); + // A window the REPL never finished, as when a hook panics before `set_contents`. + state.begin_update(JOB); + state.set(PromptSegment::Left, "Previous", JOB); + state.begin_update(JOB); + state.set(PromptSegment::Right, "Current", JOB); + + state.set_contents(PromptContents { + left: Some("BaselineLeft".into()), + right: Some("BaselineRight".into()), + ..Default::default() + }); + + let contents = state.contents(); + assert_eq!(contents.left.as_deref(), Some("BaselineLeft")); + assert_eq!(contents.right.as_deref(), Some("Current")); + } + + #[test] + fn set_contents_ends_the_update() { + let state = PromptState::new(); + state.begin_update(JOB); + state.set_contents(PromptContents::default()); + state.set(PromptSegment::Left, "AfterUpdate", JOB); + + state.set_contents(PromptContents { + left: Some("NextBaseline".into()), + ..Default::default() + }); + + assert_eq!(state.contents().left.as_deref(), Some("NextBaseline")); + } + + #[test] + fn push_from_an_older_job_is_shown_but_not_kept() { + let state = PromptState::new(); + state.begin_update(JobId::new(2)); + state.set(PromptSegment::Left, "Stale", JobId::new(1)); + assert_eq!(state.contents().left.as_deref(), Some("Stale")); + + state.set_contents(PromptContents { + left: Some("Baseline".into()), + ..Default::default() + }); + + assert_eq!(state.contents().left.as_deref(), Some("Baseline")); + } + + #[test] + fn push_from_the_main_thread_during_update_survives_set_contents() { + let state = PromptState::new(); + state.begin_update(JobId::new(2)); + state.set(PromptSegment::Left, "FromHook", JobId::ZERO); + + state.set_contents(PromptContents { + left: Some("Baseline".into()), + ..Default::default() + }); + + assert_eq!(state.contents().left.as_deref(), Some("FromHook")); + } + #[test] fn detaching_repainter_stops_repaints() { let (state, repainter_count) = setup_state_with_counter(); state.set_repainter(None); - state.set(PromptSegment::Left, "Delta"); + state.set(PromptSegment::Left, "Delta", JOB); assert_eq!(repainter_count.load(Ordering::Relaxed), 0); } diff --git a/crates/nu-std/std/prelude/mod.nu b/crates/nu-std/std/prelude/mod.nu index c3ce1d1e08..f4650e580d 100644 --- a/crates/nu-std/std/prelude/mod.nu +++ b/crates/nu-std/std/prelude/mod.nu @@ -9,12 +9,19 @@ export def banner [ ] { let foreground = $env.config.color_config?.banner_foreground? | default "attr_normal" let highlight1 = $env.config.color_config?.banner_highlight1? | default "green" -let highlight2 = $env.config.color_config?.banner_highlight2? | default "purple" -let dt = (datetime-diff (date now) 2019-05-10T09:59:12-07:00) -let ver = (version) let startup_time = $"(ansi $highlight1)(ansi attr_bold)Startup Time: (ansi reset)(ansi $foreground)($nu.startup-time)(ansi reset)" -let welcome = $"(ansi $highlight1) __ ,(ansi reset) +# The REPL prints the welcome message before the startup hooks run and the startup time right +# before the first prompt, once it is final. `--short` skips the welcome message, whose `version` +# and `datetime-diff` calls cost far more than the startup time line. +let banner_msg = if $short { + $"($startup_time)(char eol)" +} else { + let highlight2 = $env.config.color_config?.banner_highlight2? | default "purple" + let dt = (datetime-diff (date now) 2019-05-10T09:59:12-07:00) + let ver = (version) + + let welcome = $"(ansi $highlight1) __ ,(ansi reset) (ansi $highlight1) .--\(\)°'.' (ansi reset)(ansi $foreground)Welcome to (ansi $highlight1)Nushell(ansi reset)(ansi $foreground),(ansi reset) (ansi $highlight1)'|, . ,' (ansi reset)(ansi $foreground)based on the (ansi $highlight1)nu(ansi reset)(ansi $foreground) language,(ansi reset) (ansi $highlight1) !_-\(_\\ (ansi reset)(ansi $foreground)where all data is structured! @@ -30,14 +37,11 @@ let welcome = $"(ansi $highlight1) __ ,(ansi reset) (ansi $foreground)(pretty-print-duration $dt) " -# The REPL prints the welcome message before the startup hooks run and the startup time right -# before the first prompt, once it is final. -let banner_msg = if $short { - $"($startup_time)(char eol)" -} else if $no_startup_time { - $welcome -} else { - $"($welcome)(char eol)($startup_time)(ansi reset)(char eol)" + if $no_startup_time { + $welcome + } else { + $"($welcome)(char eol)($startup_time)(ansi reset)(char eol)" + } } match (config use-colors) { diff --git a/crates/nu-std/tests/test_prelude.nu b/crates/nu-std/tests/test_prelude.nu index 3d5d04aecb..d0e9741112 100644 --- a/crates/nu-std/tests/test_prelude.nu +++ b/crates/nu-std/tests/test_prelude.nu @@ -6,3 +6,23 @@ def banner [] { use std/prelude assert ((prelude banner | lines | length) == 16) } + +@test +def banner_short_is_only_the_startup_time [] { + use std/prelude + let short = prelude banner --short | ansi strip | lines + assert equal ($short | length) 1 + assert str contains $short.0 "Startup Time:" +} + +@test +def banner_short_skips_the_welcome_message [] { + use std/prelude + # `debug profile` lists the calls made. The full banner checks that it reports these two. + let full = debug profile { prelude banner } --max-depth 10 | get source + assert ("version" in $full) + assert ("datetime-diff" in $full) + let short = debug profile { prelude banner --short } --max-depth 10 | get source + assert ("version" not-in $short) + assert ("datetime-diff" not-in $short) +} diff --git a/scripts/build.rs b/scripts/build.rs index 1469b50418..46437342ef 100644 --- a/scripts/build.rs +++ b/scripts/build.rs @@ -1,5 +1,6 @@ fn main() { let target_os = std::env::var("CARGO_CFG_TARGET_OS").expect("set by cargo"); + let target_arch = std::env::var("CARGO_CFG_TARGET_ARCH").expect("set by cargo"); println!("cargo:rerun-if-changed=scripts/build.rs"); println!( "cargo:rustc-env=NU_FEATURES={}", @@ -13,6 +14,21 @@ fn main() { res.compile() .expect("Failed to run the Windows resource compiler (rc.exe)"); } else { + if target_os == "macos" + && target_arch == "aarch64" + && std::env::var_os("RUSTC_LINKER").is_none() + { + // Link with chained fixups. At Rust's default deployment target (macOS 11) the linker + // writes classic rebase opcodes instead, and dyld rewrites every pointer in the + // binary's constant data before `main` runs. Since macOS 13 the kernel applies chained + // fixups page by page as pages are first touched, so startup only pays for the pages + // it uses. macOS 11 is the first release that loads them. That is why this is limited + // to aarch64: every arm64 Mac runs macOS 11 or later, but x86_64's default deployment + // target (10.12) predates them, and the binary would fail to load on 10.12-10.15. + // Cargo sets `RUSTC_LINKER` only when the build configures its own linker, and some, + // such as `zig cc` (cargo-zigbuild), reject the flag, so only Apple's default gets it. + println!("cargo:rustc-link-arg-bins=-Wl,-fixup_chains"); + } // Tango uses dynamic linking, to allow us to dynamically change between two bench suit at runtime. // This is currently not supported on non nightly rust, on windows. println!("cargo:rustc-link-arg-benches=-rdynamic");