Remove nu-glob's dependency on nu-protocol (#15349)

# Description

This PR solves a circular dependency issue (`nu-test-support` needs
`nu-glob` which needs `nu-protocol` which needs `nu-test-support`). This
was done by making the glob functions that any type that implements
`Interruptible` to remove the dependency on `Signals`.

# After Submitting

Make `Paths.next()` a O(1) operation so that cancellation/interrupt
handling can be moved to the caller (e.g., by wrapping the `Paths`
iterator in a cancellation iterator).
This commit is contained in:
Ian Manske 2025-03-20 09:32:41 -07:00 committed by GitHub
parent b241e9edd5
commit dfba62da00
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 81 additions and 59 deletions

2
Cargo.lock generated
View File

@ -3795,7 +3795,6 @@ name = "nu-glob"
version = "0.103.1"
dependencies = [
"doc-comment",
"nu-protocol",
]
[[package]]
@ -3967,6 +3966,7 @@ dependencies = [
"miette",
"nix 0.29.0",
"nu-derive-value",
"nu-glob",
"nu-path",
"nu-system",
"nu-test-support",

View File

@ -158,17 +158,15 @@ impl Command for UTouch {
continue;
}
let mut expanded_globs = glob(
&file_path.to_string_lossy(),
Some(engine_state.signals().clone()),
)
.unwrap_or_else(|_| {
panic!(
"Failed to process file path: {}",
&file_path.to_string_lossy()
)
})
.peekable();
let mut expanded_globs =
glob(&file_path.to_string_lossy(), engine_state.signals().clone())
.unwrap_or_else(|_| {
panic!(
"Failed to process file path: {}",
&file_path.to_string_lossy()
)
})
.peekable();
if expanded_globs.peek().is_none() {
let file_name = file_path.file_name().unwrap_or_else(|| {

View File

@ -14,10 +14,9 @@ categories = ["filesystem"]
bench = false
[dependencies]
nu-protocol = { path = "../nu-protocol", version = "0.103.1", default-features = false }
[dev-dependencies]
doc-comment = "0.3"
[lints]
workspace = true
workspace = true

View File

@ -27,9 +27,9 @@
//! To print all jpg files in `/media/` and all of its subdirectories.
//!
//! ```rust,no_run
//! use nu_glob::glob;
//! use nu_glob::{glob, Uninterruptible};
//!
//! for entry in glob("/media/**/*.jpg", None).expect("Failed to read glob pattern") {
//! for entry in glob("/media/**/*.jpg", Uninterruptible).expect("Failed to read glob pattern") {
//! match entry {
//! Ok(path) => println!("{:?}", path.display()),
//! Err(e) => println!("{:?}", e),
@ -42,9 +42,7 @@
//! instead of printing them.
//!
//! ```rust,no_run
//! use nu_glob::glob_with;
//! use nu_glob::MatchOptions;
//! use nu_protocol::Signals;
//! use nu_glob::{glob_with, MatchOptions, Uninterruptible};
//!
//! let options = MatchOptions {
//! case_sensitive: false,
@ -52,7 +50,7 @@
//! require_literal_leading_dot: false,
//! recursive_match_hidden_dir: true,
//! };
//! for entry in glob_with("local/*a*", options, Signals::empty()).unwrap() {
//! for entry in glob_with("local/*a*", options, Uninterruptible).unwrap() {
//! if let Ok(path) = entry {
//! println!("{:?}", path.display())
//! }
@ -73,7 +71,6 @@ extern crate doc_comment;
#[cfg(test)]
doctest!("../README.md");
use nu_protocol::Signals;
use std::cmp;
use std::cmp::Ordering;
use std::error::Error;
@ -88,6 +85,29 @@ use MatchResult::{EntirePatternDoesntMatch, Match, SubPatternDoesntMatch};
use PatternToken::AnyExcept;
use PatternToken::{AnyChar, AnyRecursiveSequence, AnySequence, AnyWithin, Char};
/// A trait for types that can be periodically polled to check whether to cancel an operation.
pub trait Interruptible {
/// Returns whether the current operation should be cancelled.
fn interrupted(&self) -> bool;
}
impl<I: Interruptible> Interruptible for &I {
#[inline]
fn interrupted(&self) -> bool {
(*self).interrupted()
}
}
/// A no-op implementor of [`Interruptible`] that always returns `false` for [`interrupted`](Interruptible::interrupted).
pub struct Uninterruptible;
impl Interruptible for Uninterruptible {
#[inline]
fn interrupted(&self) -> bool {
false
}
}
/// An iterator that yields `Path`s from the filesystem that match a particular
/// pattern.
///
@ -98,16 +118,16 @@ use PatternToken::{AnyChar, AnyRecursiveSequence, AnySequence, AnyWithin, Char};
///
/// See the `glob` function for more details.
#[derive(Debug)]
pub struct Paths {
pub struct Paths<I = Uninterruptible> {
dir_patterns: Vec<Pattern>,
require_dir: bool,
options: MatchOptions,
todo: Vec<Result<(PathBuf, usize), GlobError>>,
scope: Option<PathBuf>,
signals: Signals,
interrupt: I,
}
impl Paths {
impl Paths<Uninterruptible> {
/// An iterator representing a single path.
pub fn single(path: &Path, relative_to: &Path) -> Self {
Paths {
@ -116,7 +136,7 @@ impl Paths {
options: MatchOptions::default(),
todo: vec![Ok((path.to_path_buf(), 0))],
scope: Some(relative_to.into()),
signals: Signals::empty(),
interrupt: Uninterruptible,
}
}
}
@ -133,7 +153,7 @@ impl Paths {
///
/// When iterating, each result is a `GlobResult` which expresses the
/// possibility that there was an `IoError` when attempting to read the contents
/// of the matched path. In other words, each item returned by the iterator
/// of the matched path. In other words, each item returned by the iterator
/// will either be an `Ok(Path)` if the path matched, or an `Err(GlobError)` if
/// the path (partially) matched _but_ its contents could not be read in order
/// to determine if its contents matched.
@ -146,9 +166,9 @@ impl Paths {
/// `kittens.jpg`, `puppies.jpg` and `hamsters.gif`:
///
/// ```rust,no_run
/// use nu_glob::glob;
/// use nu_glob::{glob, Uninterruptible};
///
/// for entry in glob("/media/pictures/*.jpg", None).unwrap() {
/// for entry in glob("/media/pictures/*.jpg", Uninterruptible).unwrap() {
/// match entry {
/// Ok(path) => println!("{:?}", path.display()),
///
@ -170,20 +190,16 @@ impl Paths {
/// `filter_map`:
///
/// ```rust
/// use nu_glob::glob;
/// use nu_glob::{glob, Uninterruptible};
/// use std::result::Result;
///
/// for path in glob("/media/pictures/*.jpg", None).unwrap().filter_map(Result::ok) {
/// for path in glob("/media/pictures/*.jpg", Uninterruptible).unwrap().filter_map(Result::ok) {
/// println!("{}", path.display());
/// }
/// ```
/// Paths are yielded in alphabetical order.
pub fn glob(pattern: &str, signals: Option<Signals>) -> Result<Paths, PatternError> {
glob_with(
pattern,
MatchOptions::default(),
signals.unwrap_or(Signals::empty()),
)
pub fn glob<I: Interruptible>(pattern: &str, interrupt: I) -> Result<Paths<I>, PatternError> {
glob_with(pattern, MatchOptions::default(), interrupt)
}
/// Return an iterator that produces all the `Path`s that match the given
@ -199,11 +215,11 @@ pub fn glob(pattern: &str, signals: Option<Signals>) -> Result<Paths, PatternErr
/// passed to this function.
///
/// Paths are yielded in alphabetical order.
pub fn glob_with(
pub fn glob_with<I: Interruptible>(
pattern: &str,
options: MatchOptions,
signals: Signals,
) -> Result<Paths, PatternError> {
interrupt: I,
) -> Result<Paths<I>, PatternError> {
#[cfg(windows)]
fn check_windows_verbatim(p: &Path) -> bool {
match p.components().next() {
@ -265,7 +281,7 @@ pub fn glob_with(
options,
todo: Vec::new(),
scope: None,
signals,
interrupt,
});
}
@ -297,7 +313,7 @@ pub fn glob_with(
options,
todo,
scope: Some(scope),
signals,
interrupt,
})
}
@ -308,13 +324,13 @@ pub fn glob_with(
/// This is provided primarily for testability, so multithreaded test runners can
/// test pattern matches in different test directories at the same time without
/// having to append the parent to the pattern under test.
pub fn glob_with_parent(
pub fn glob_with_parent<I: Interruptible>(
pattern: &str,
options: MatchOptions,
parent: &Path,
signals: Signals,
) -> Result<Paths, PatternError> {
match glob_with(pattern, options, signals) {
interrupt: I,
) -> Result<Paths<I>, PatternError> {
match glob_with(pattern, options, interrupt) {
Ok(mut p) => {
p.scope = match p.scope {
None => Some(parent.to_path_buf()),
@ -408,7 +424,7 @@ fn is_dir(p: &Path) -> bool {
/// such as failing to read a particular directory's contents.
pub type GlobResult = Result<PathBuf, GlobError>;
impl Iterator for Paths {
impl<I: Interruptible> Iterator for Paths<I> {
type Item = GlobResult;
fn next(&mut self) -> Option<GlobResult> {
@ -429,7 +445,7 @@ impl Iterator for Paths {
0,
&scope,
self.options,
&self.signals,
&self.interrupt,
);
}
}
@ -487,7 +503,7 @@ impl Iterator for Paths {
next,
&path,
self.options,
&self.signals,
&self.interrupt,
);
if next == self.dir_patterns.len() - 1 {
@ -539,7 +555,7 @@ impl Iterator for Paths {
idx + 1,
&path,
self.options,
&self.signals,
&self.interrupt,
);
}
}
@ -929,7 +945,7 @@ fn fill_todo(
idx: usize,
path: &Path,
options: MatchOptions,
signals: &Signals,
interrupt: &impl Interruptible,
) {
// convert a pattern that's just many Char(_) to a string
fn pattern_as_str(pattern: &Pattern) -> Option<String> {
@ -951,7 +967,7 @@ fn fill_todo(
// . or .. globs since these never show up as path components.
todo.push(Ok((next_path, !0)));
} else {
fill_todo(todo, patterns, idx + 1, &next_path, options, signals);
fill_todo(todo, patterns, idx + 1, &next_path, options, interrupt);
}
};
@ -982,7 +998,7 @@ fn fill_todo(
None if is_dir => {
let dirs = fs::read_dir(path).and_then(|d| {
d.map(|e| {
if signals.interrupted() {
if interrupt.interrupted() {
return Err(io::Error::from(io::ErrorKind::Interrupted));
}
e.map(|e| {
@ -1141,13 +1157,13 @@ impl Default for MatchOptions {
#[cfg(test)]
mod test {
use crate::{Paths, PatternError};
use crate::{Paths, PatternError, Uninterruptible};
use super::{glob as glob_with_signals, MatchOptions, Pattern};
use std::path::Path;
fn glob(pattern: &str) -> Result<Paths, PatternError> {
glob_with_signals(pattern, None)
glob_with_signals(pattern, Uninterruptible)
}
#[test]

View File

@ -9,6 +9,7 @@ use lsp_types::{
TextDocumentPositionParams, TextEdit, Uri, WorkspaceEdit, WorkspaceFolder,
};
use miette::{miette, IntoDiagnostic, Result};
use nu_glob::Uninterruptible;
use nu_protocol::{
engine::{EngineState, StateWorkingSet},
Span,
@ -42,7 +43,7 @@ fn find_nu_scripts_in_folder(folder_uri: &Uri) -> Result<nu_glob::Paths> {
return Err(miette!("\nworkspace folder does not exist."));
}
let pattern = format!("{}/**/*.nu", path.to_string_lossy());
nu_glob::glob(&pattern, None).into_diagnostic()
nu_glob::glob(&pattern, Uninterruptible).into_diagnostic()
}
impl LanguageServer {

View File

@ -17,6 +17,7 @@ workspace = true
[dependencies]
nu-utils = { path = "../nu-utils", version = "0.103.1", default-features = false }
nu-glob = { path = "../nu-glob", version = "0.103.1" }
nu-path = { path = "../nu-path", version = "0.103.1" }
nu-system = { path = "../nu-system", version = "0.103.1" }
nu-derive-value = { path = "../nu-derive-value", version = "0.103.1" }

View File

@ -1,11 +1,11 @@
use crate::{ShellError, Span};
use nu_glob::Interruptible;
use serde::{Deserialize, Serialize};
use std::sync::{
atomic::{AtomicBool, Ordering},
Arc,
};
use serde::{Deserialize, Serialize};
/// Used to check for signals to suspend or terminate the execution of Nushell code.
///
/// For now, this struct only supports interruption (ctrl+c or SIGINT).
@ -84,6 +84,13 @@ impl Signals {
}
}
impl Interruptible for Signals {
#[inline]
fn interrupted(&self) -> bool {
self.interrupted()
}
}
/// The types of things that can be signaled. It's anticipated this will change as we learn more
/// about how we'd like signals to be handled.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]

View File

@ -1,6 +1,6 @@
use super::Director;
use crate::fs::{self, Stub};
use nu_glob::glob;
use nu_glob::{glob, Uninterruptible};
#[cfg(not(target_arch = "wasm32"))]
use nu_path::Path;
use nu_path::{AbsolutePath, AbsolutePathBuf};
@ -231,7 +231,7 @@ impl Playground<'_> {
}
pub fn glob_vec(pattern: &str) -> Vec<std::path::PathBuf> {
let glob = glob(pattern, None);
let glob = glob(pattern, Uninterruptible);
glob.expect("invalid pattern")
.map(|path| {