mirror of
https://github.com/nushell/nushell.git
synced 2026-10-28 08:54:52 +08:00
Faster startup: chained fixups on Apple silicon, a cheaper banner, and async prompt pushes that survive (#19236)
## Description Three independent changes make the REPL ready to type into sooner. With the default config, launching `nu` until the first prompt is drawn drops from 15.7 ms to 13.1 ms (−17%). `$nu.startup-time` is measured the same way as before. Measurements: | default config, from `~` | main | this PR | | --- | --- | --- | | launch to first prompt | 15.70 ms | 13.10 ms | | `$nu.startup-time` | 8.03 ms | 7.36 ms | | instructions retired by the first prompt | 149.3M | 119.8M | 1. Change the linker on Apple silicon to `"cargo:rustc-link-arg-bins=-Wl,-fixup_chains"` 2. Make `banner` cheaper (all architectures) 3. Keep prompt pushes that land before the prompt is rebuilt (all architectures) ## User-facing changes (Release notes) - Improved startup time. On Apple silicon Macs, `nu` is now linked with chained fixups, so less work happens before Nushell's own code starts and each `nu` process uses about 3 MiB less memory. The startup banner no longer computes the welcome message when it only prints the startup time. - `date to-timezone` (all architectures) now accepts IANA time zone names in any letter case, for example `date now | date to-timezone america/new_york`. Before, some lowercase names were rejected as invalid. - Background jobs that update the prompt with `commandline set-prompt` (all architectures), started from a `pre_prompt` or `env_change` hook or from a prompt closure such as `$env.PROMPT_COMMAND`, no longer lose that update when they finish before the prompt is drawn. A `commandline set-prompt` called directly inside one of those hooks or prompt closures now also shows on that prompt, in place of the value the prompt variables give that segment. Before, the prompt evaluation that follows the hooks always replaced it. ## Additional notes <!-- Optional. Examples: - fixes #123 - closes #456 - related #789 Anything else reviewers should know. Remove this section if not needed. -->
This commit is contained in:
1 parent
d211796628
commit
d88d4fe0e1
17 files changed
+376
-109
No files matched your search
Generated
-10
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
mod commandline_set_prompt;
|
||||
mod keybindings_list;
|
||||
mod nu_highlight;
|
||||
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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<Tz, ParseErrorKind> {
|
||||
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(
|
||||
|
||||
@@ -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))),
|
||||
};
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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))),
|
||||
};
|
||||
|
||||
@@ -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<ThreadJob>,
|
||||
|
||||
@@ -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<dyn Fn() + Send + Sync>;
|
||||
@@ -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<PendingPushes>,
|
||||
}
|
||||
|
||||
/// 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<PromptContents>,
|
||||
/// The pending pushes share the lock so a push can't slip between them and the contents.
|
||||
data: RwLock<PromptData>,
|
||||
|
||||
/// 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<Option<Repainter>>,
|
||||
}
|
||||
@@ -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<ReturnType>(
|
||||
&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<Arc<str>>) {
|
||||
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<Arc<str>>, 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<PromptState>, Arc<AtomicUsize>) {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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");
|
||||
|
||||
Reference in new issue
Block a user