first pass of merging in warp (doesn't build)
This commit is contained in:
@@ -33,7 +33,7 @@ thiserror.workspace = true
|
||||
galaxy_cli.workspace = true
|
||||
galaxy_core.workspace = true
|
||||
galaxy_js = { workspace = true, optional = true }
|
||||
galaxyui.workspace = true
|
||||
galaxyui_core.workspace = true
|
||||
galaxy_util.workspace = true
|
||||
typed-path.workspace = true
|
||||
|
||||
|
||||
@@ -3,9 +3,8 @@ use std::collections::HashMap;
|
||||
|
||||
use itertools::Itertools;
|
||||
|
||||
use crate::completer::suggest::SuggestionTypeName;
|
||||
|
||||
use super::suggest::{MatchedSuggestion, Priority};
|
||||
use crate::completer::suggest::SuggestionTypeName;
|
||||
|
||||
/// Given a map of computed, unordered suggestion vectors keyed by `SuggestionType`, returns a
|
||||
/// single vector of suggestions in order.
|
||||
|
||||
@@ -15,10 +15,11 @@ use galaxy_util::path::{EscapeChar, ShellFamily};
|
||||
use galaxyui::platform::OperatingSystem;
|
||||
use smol_str::SmolStr;
|
||||
use typed_path::{TypedPath, TypedPathBuf};
|
||||
|
||||
use crate::{completer::TopLevelCommandCaseSensitivity, signatures::CommandRegistry};
|
||||
use galaxyui_core::platform::OperatingSystem;
|
||||
|
||||
use super::engine::EngineDirEntry;
|
||||
use crate::completer::TopLevelCommandCaseSensitivity;
|
||||
use crate::signatures::CommandRegistry;
|
||||
|
||||
/// This trait may be implemented to configure behavior of the completions engine.
|
||||
pub trait CompletionContext: Send + Sync {
|
||||
@@ -164,6 +165,11 @@ pub trait PathCompletionContext: Send + Sync {
|
||||
/// This is used to expand '~' and '$HOME' in user input.
|
||||
fn home_directory(&self) -> Option<&str>;
|
||||
|
||||
/// `CDPATH`/`cdpath` from the shell, colon-separated. Used to extend `cd` argument completions.
|
||||
fn cdpath(&self) -> Option<&str> {
|
||||
None
|
||||
}
|
||||
|
||||
fn shell_family(&self) -> ShellFamily;
|
||||
|
||||
/// The current working directory, which is used to determine how relative path suggestions
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use async_trait::async_trait;
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::Serialize;
|
||||
use galaxy_js::{JsFunctionId, SerializedJsValue, TypedJsFunctionRef};
|
||||
use serde::{de::DeserializeOwned, Serialize};
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum JsExecutionError {
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
use galaxyui::platform::OperatingSystem;
|
||||
use itertools::Itertools;
|
||||
use string_offset::ByteOffset;
|
||||
use galaxyui_core::platform::OperatingSystem;
|
||||
|
||||
use crate::{
|
||||
completer::suggest::MatchRequirement,
|
||||
meta::{HasSpan, Span, Spanned},
|
||||
};
|
||||
use crate::{meta::SpannedItem, parsers::simple::command_at_cursor_position};
|
||||
|
||||
use super::context::CompletionContext;
|
||||
use super::suggest::{suggestions, CompleterOptions, CompletionsFallbackStrategy, SuggestionType};
|
||||
use super::{context::CompletionContext, get_path_separators};
|
||||
use super::{Match, MatchStrategy};
|
||||
use super::{get_path_separators, Match, MatchStrategy};
|
||||
use crate::completer::suggest::MatchRequirement;
|
||||
use crate::meta::{HasSpan, Span, Spanned, SpannedItem};
|
||||
use crate::parsers::simple::command_at_cursor_position;
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Clone)]
|
||||
pub struct Description {
|
||||
@@ -258,5 +256,5 @@ fn floor_char_boundary(original_string: &str, idx: usize) -> usize {
|
||||
}
|
||||
|
||||
#[cfg(all(test, not(feature = "v2")))]
|
||||
#[path = "describe_test.rs"]
|
||||
#[path = "describe_tests.rs"]
|
||||
mod tests;
|
||||
|
||||
@@ -4,20 +4,17 @@ use std::iter::FromIterator;
|
||||
use string_offset::ByteOffset;
|
||||
use typed_path::TypedPathBuf;
|
||||
|
||||
use crate::completer::EngineDirEntry;
|
||||
use crate::completer::{context::CompletionContext, suggest::MatchRequirement};
|
||||
use crate::completer::{
|
||||
describe::OptionCaseSensitivity,
|
||||
testing::{FakeCompletionContext, MockPathCompletionContext},
|
||||
};
|
||||
use crate::completer::{suggest::SuggestionType, TopLevelCommandCaseSensitivity};
|
||||
use crate::meta::{Span, SpannedItem};
|
||||
use crate::signatures::{
|
||||
testing::{add_content_signature, create_test_command_registry, git_signature, test_signature},
|
||||
CommandRegistry,
|
||||
};
|
||||
|
||||
use super::{describe, Description};
|
||||
use crate::completer::context::CompletionContext;
|
||||
use crate::completer::describe::OptionCaseSensitivity;
|
||||
use crate::completer::suggest::{MatchRequirement, SuggestionType};
|
||||
use crate::completer::testing::{FakeCompletionContext, MockPathCompletionContext};
|
||||
use crate::completer::{EngineDirEntry, TopLevelCommandCaseSensitivity};
|
||||
use crate::meta::{Span, SpannedItem};
|
||||
use crate::signatures::testing::{
|
||||
add_content_signature, create_test_command_registry, git_signature, test_signature,
|
||||
};
|
||||
use crate::signatures::CommandRegistry;
|
||||
|
||||
#[cfg(windows)]
|
||||
mod windows_constants {
|
||||
@@ -42,7 +39,7 @@ fn describe_at_cursor<T: CompletionContext>(
|
||||
pos: ByteOffset,
|
||||
ctx: &T,
|
||||
) -> Option<Description> {
|
||||
galaxyui::r#async::block_on(describe(line, pos, ctx))
|
||||
galaxyui_core::r#async::block_on(describe(line, pos, ctx))
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -0,0 +1,533 @@
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::iter::FromIterator;
|
||||
|
||||
use string_offset::ByteOffset;
|
||||
use typed_path::TypedPathBuf;
|
||||
|
||||
use super::{describe, Description};
|
||||
use crate::completer::context::CompletionContext;
|
||||
use crate::completer::describe::OptionCaseSensitivity;
|
||||
use crate::completer::suggest::{MatchRequirement, SuggestionType};
|
||||
use crate::completer::testing::{FakeCompletionContext, MockPathCompletionContext};
|
||||
use crate::completer::{EngineDirEntry, TopLevelCommandCaseSensitivity};
|
||||
use crate::meta::{Span, SpannedItem};
|
||||
use crate::signatures::testing::{
|
||||
add_content_signature, create_test_command_registry, git_signature, test_signature,
|
||||
};
|
||||
use crate::signatures::CommandRegistry;
|
||||
|
||||
#[cfg(windows)]
|
||||
mod windows_constants {
|
||||
pub(super) const TEST_WORK_DIR: &str = r"C:\";
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
use windows_constants::*;
|
||||
|
||||
#[cfg(unix)]
|
||||
mod unix_constants {
|
||||
pub(super) const TEST_WORK_DIR: &str = "/home/";
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
use unix_constants::*;
|
||||
|
||||
/// Given a line and position in the line, runs the completer at the position and returns
|
||||
/// a Description struct for the word at pos
|
||||
fn describe_at_cursor<T: CompletionContext>(
|
||||
line: &str,
|
||||
pos: ByteOffset,
|
||||
ctx: &T,
|
||||
) -> Option<Description> {
|
||||
galaxyui_core::r#async::block_on(describe(line, pos, ctx))
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn test_describe_top_level_commands_case_sensitive() {
|
||||
let ctx = FakeCompletionContext::new(CommandRegistry::default())
|
||||
.with_case_sensitivity()
|
||||
.with_top_level_commands(vec!["git", "networkQuality"]);
|
||||
|
||||
assert_eq!(
|
||||
describe_at_cursor("git", ByteOffset::from(1), &ctx).map(Description::into_token_name),
|
||||
Some("git".into())
|
||||
);
|
||||
|
||||
assert!(describe_at_cursor("GIT", ByteOffset::from(1), &ctx)
|
||||
.map(Description::into_token_name)
|
||||
.is_none());
|
||||
|
||||
assert!(describe_at_cursor("GIt", ByteOffset::from(1), &ctx)
|
||||
.map(Description::into_token_name)
|
||||
.is_none());
|
||||
|
||||
// The `TopLevelCommandCaseSensitivity` value does not matter since we check parts other than the top-level command.
|
||||
assert_eq!(
|
||||
describe_at_cursor("git status", ByteOffset::from(4), &ctx)
|
||||
.map(Description::into_token_name),
|
||||
Some("status".into())
|
||||
);
|
||||
|
||||
// There should be no descriptions for `git Status` since `Status` is not a valid
|
||||
// subcommand.
|
||||
assert!(describe_at_cursor("git Status", ByteOffset::from(4), &ctx).is_none());
|
||||
|
||||
assert_eq!(
|
||||
describe_at_cursor("git status --ahead-behind", ByteOffset::from(14), &ctx)
|
||||
.map(Description::into_token_name),
|
||||
Some("--ahead-behind".into())
|
||||
);
|
||||
|
||||
assert!(describe_at_cursor("git status --AHEAD-behind", ByteOffset::from(14), &ctx).is_none());
|
||||
|
||||
assert_eq!(
|
||||
describe_at_cursor("networkQuality", ByteOffset::from(1), &ctx)
|
||||
.map(Description::into_token_name),
|
||||
Some("networkQuality".into())
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn test_describe_top_level_commands_case_insensitive() {
|
||||
let ctx = FakeCompletionContext::new(CommandRegistry::default())
|
||||
.with_top_level_commands(vec!["git", "networkQuality"]);
|
||||
|
||||
assert_eq!(
|
||||
describe_at_cursor("git", ByteOffset::from(1), &ctx).map(Description::into_token_name),
|
||||
Some("git".into())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
describe_at_cursor("GIT", ByteOffset::from(1), &ctx).map(Description::into_token_name),
|
||||
Some("git".into())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
describe_at_cursor("GIt", ByteOffset::from(1), &ctx).map(Description::into_token_name),
|
||||
Some("git".into())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
describe_at_cursor("git status && GIT checkout", ByteOffset::from(15), &ctx)
|
||||
.map(Description::into_token_name),
|
||||
Some("git".into())
|
||||
);
|
||||
|
||||
// The `TopLevelCommandCaseSensitivity` value does not matter since we check parts other than the top-level command.
|
||||
assert_eq!(
|
||||
describe_at_cursor("git status", ByteOffset::from(4), &ctx)
|
||||
.map(Description::into_token_name),
|
||||
Some("status".into())
|
||||
);
|
||||
|
||||
// There should be no descriptions for `git Status` since `Status` is not a valid
|
||||
// subcommand.
|
||||
assert!(describe_at_cursor("git Status", ByteOffset::from(4), &ctx).is_none());
|
||||
|
||||
assert_eq!(
|
||||
describe_at_cursor("git status --ahead-behind", ByteOffset::from(14), &ctx)
|
||||
.map(Description::into_token_name),
|
||||
Some("--ahead-behind".into())
|
||||
);
|
||||
|
||||
assert!(describe_at_cursor("git status --AHEAD-behind", ByteOffset::from(14), &ctx).is_none());
|
||||
|
||||
assert_eq!(
|
||||
describe_at_cursor("networkQuality", ByteOffset::from(1), &ctx)
|
||||
.map(Description::into_token_name),
|
||||
Some("networkQuality".into())
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn test_xray_describe() {
|
||||
let ctx = FakeCompletionContext::new(CommandRegistry::default())
|
||||
.with_top_level_commands(["git"])
|
||||
.with_environment_variable_names(HashSet::from(["HOME".into()]));
|
||||
|
||||
let line = r"git status $(git stash) && git checkout main && $HOME";
|
||||
|
||||
assert_eq!(
|
||||
describe_at_cursor(line, ByteOffset::from(1), &ctx),
|
||||
Some(Description {
|
||||
token: "git".to_string().spanned(Span::new(0, 3)),
|
||||
description_text: Some("The stupid content tracker".to_string()),
|
||||
suggestion_type: SuggestionType::Command(
|
||||
TopLevelCommandCaseSensitivity::CaseInsensitive
|
||||
),
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
describe_at_cursor(line, ByteOffset::from(5), &ctx),
|
||||
Some(Description {
|
||||
token: "status".to_string().spanned(Span::new(4, 10)),
|
||||
description_text: Some("Show the working tree status".to_string()),
|
||||
suggestion_type: SuggestionType::Subcommand
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
describe_at_cursor(line, ByteOffset::from(13), &ctx),
|
||||
Some(Description {
|
||||
token: "git".to_string().spanned(Span::new(13, 16)),
|
||||
description_text: Some("The stupid content tracker".to_string()),
|
||||
suggestion_type: SuggestionType::Command(
|
||||
TopLevelCommandCaseSensitivity::CaseInsensitive
|
||||
),
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
describe_at_cursor(line, ByteOffset::from(18), &ctx),
|
||||
Some(Description {
|
||||
token: "stash".to_string().spanned(Span::new(17, 22)),
|
||||
description_text: Some("Temporarily stores all the modified tracked files".to_string()),
|
||||
suggestion_type: SuggestionType::Subcommand
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
describe_at_cursor(line, ByteOffset::from(28), &ctx),
|
||||
Some(Description {
|
||||
token: "git".to_string().spanned(Span::new(27, 30)),
|
||||
description_text: Some("The stupid content tracker".to_string()),
|
||||
suggestion_type: SuggestionType::Command(
|
||||
TopLevelCommandCaseSensitivity::CaseInsensitive
|
||||
),
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
describe_at_cursor(line, ByteOffset::from(33), &ctx),
|
||||
Some(Description {
|
||||
token: "checkout".to_string().spanned(Span::new(31, 39)),
|
||||
description_text: Some("Switch branches or restore working tree files".to_string()),
|
||||
suggestion_type: SuggestionType::Subcommand
|
||||
},)
|
||||
);
|
||||
assert_eq!(
|
||||
describe_at_cursor(line, ByteOffset::from(50), &ctx),
|
||||
Some(Description {
|
||||
token: "$HOME".to_string().spanned(Span::new(48, 53)),
|
||||
description_text: None,
|
||||
suggestion_type: SuggestionType::Variable
|
||||
},)
|
||||
);
|
||||
assert!(describe_at_cursor(line, ByteOffset::from(25), &ctx).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn test_xray_describe_with_flags() {
|
||||
let ctx = FakeCompletionContext::new(CommandRegistry::default());
|
||||
|
||||
let mut line = r"git commit -am";
|
||||
assert_eq!(
|
||||
describe_at_cursor(line, ByteOffset::from(11), &ctx),
|
||||
Some(Description {
|
||||
token: "-am".to_string().spanned(Span::new(11, 14)),
|
||||
description_text: Some("Use the given message as the commit message".to_string()),
|
||||
suggestion_type: SuggestionType::Option(
|
||||
MatchRequirement::EntireName,
|
||||
OptionCaseSensitivity::CaseSensitive
|
||||
)
|
||||
})
|
||||
);
|
||||
|
||||
line = "git commit -a";
|
||||
assert_eq!(
|
||||
describe_at_cursor(line, ByteOffset::from(11), &ctx),
|
||||
Some(Description {
|
||||
token: "-a".to_string().spanned(Span::new(11, 13)),
|
||||
description_text: Some("Stage all modified and deleted paths".to_string()),
|
||||
suggestion_type: SuggestionType::Option(
|
||||
MatchRequirement::EntireName,
|
||||
OptionCaseSensitivity::CaseSensitive
|
||||
)
|
||||
})
|
||||
);
|
||||
|
||||
line = "git commit --all";
|
||||
assert_eq!(
|
||||
describe_at_cursor(line, ByteOffset::from(11), &ctx),
|
||||
Some(Description {
|
||||
token: "--all".to_string().spanned(Span::new(11, 16)),
|
||||
description_text: Some("Stage all modified and deleted paths".to_string()),
|
||||
suggestion_type: SuggestionType::Option(
|
||||
MatchRequirement::EntireName,
|
||||
OptionCaseSensitivity::CaseSensitive
|
||||
)
|
||||
})
|
||||
);
|
||||
|
||||
line = "git commit --All";
|
||||
assert_eq!(describe_at_cursor(line, ByteOffset::from(11), &ctx), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn test_xray_describe_with_directories() {
|
||||
let pwd = TypedPathBuf::from(TEST_WORK_DIR);
|
||||
let path_ctx = MockPathCompletionContext::new(pwd.clone())
|
||||
.with_entries_in_pwd([
|
||||
EngineDirEntry::test_dir("foo"),
|
||||
EngineDirEntry::test_file("foobar"),
|
||||
])
|
||||
.with_entries(pwd.join("foo/"), [EngineDirEntry::test_dir("src")])
|
||||
.with_entries(pwd.join("foo/src/"), [EngineDirEntry::test_file("bar")]);
|
||||
|
||||
let ctx = FakeCompletionContext::new(CommandRegistry::default())
|
||||
.with_path_completion_context(path_ctx);
|
||||
|
||||
let mut line = r"ls foo/ && cd foo/src";
|
||||
|
||||
assert_eq!(
|
||||
describe_at_cursor(line, ByteOffset::from(5), &ctx),
|
||||
Some(Description {
|
||||
token: "foo/".to_string().spanned(Span::new(3, 7)),
|
||||
description_text: Some("Directory".to_string()),
|
||||
suggestion_type: SuggestionType::Argument
|
||||
})
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
describe_at_cursor(line, ByteOffset::from(17), &ctx),
|
||||
Some(Description {
|
||||
token: "foo/src".to_string().spanned(Span::new(14, 21)),
|
||||
description_text: Some("Directory".to_string()),
|
||||
suggestion_type: SuggestionType::Argument,
|
||||
})
|
||||
);
|
||||
|
||||
line = r"cat foo/src/bar && cat foobar";
|
||||
|
||||
assert_eq!(
|
||||
describe_at_cursor(line, ByteOffset::from(5), &ctx),
|
||||
Some(Description {
|
||||
token: "foo/src/bar".to_string().spanned(Span::new(4, 15)),
|
||||
description_text: Some("File".to_string()),
|
||||
suggestion_type: SuggestionType::Argument
|
||||
})
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
describe_at_cursor(line, ByteOffset::from(25), &ctx),
|
||||
Some(Description {
|
||||
token: "foobar".to_string().spanned(Span::new(23, 29)),
|
||||
description_text: Some("File".to_string()),
|
||||
suggestion_type: SuggestionType::Argument,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/// Regression test for linear issues WAR-4244 and WAR-4245
|
||||
#[test]
|
||||
pub fn test_xray_describe_with_non_ascii_chars() {
|
||||
let registry = create_test_command_registry([git_signature()]);
|
||||
let ctx = FakeCompletionContext::new(registry);
|
||||
|
||||
let line = r"漢字 && git checkout 漢字";
|
||||
|
||||
assert_eq!(
|
||||
describe_at_cursor(line, ByteOffset::from(24), &ctx),
|
||||
Some(Description {
|
||||
token: "漢字".to_string().spanned(Span::new(23, 29)),
|
||||
description_text: None,
|
||||
suggestion_type: SuggestionType::Argument,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn test_xray_describe_single_char_line() {
|
||||
let aliases = HashMap::from_iter([("g".into(), "git".into())]);
|
||||
let ctx = FakeCompletionContext::new(CommandRegistry::default())
|
||||
.with_aliases(aliases.clone())
|
||||
.with_top_level_commands(aliases.into_keys());
|
||||
|
||||
let line = "g";
|
||||
|
||||
assert_eq!(
|
||||
describe_at_cursor(line, ByteOffset::from(0), &ctx),
|
||||
Some(Description {
|
||||
token: "g".to_string().spanned(Span::new(0, 1)),
|
||||
description_text: Some("Alias for \"git\"".to_string()),
|
||||
suggestion_type: SuggestionType::Command(TopLevelCommandCaseSensitivity::CaseSensitive),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn test_xray_describe_ndots() {
|
||||
let aliases = HashMap::from_iter([("...".into(), "cd ../../".into())]);
|
||||
let ctx = FakeCompletionContext::new(CommandRegistry::default())
|
||||
.with_aliases(aliases.clone())
|
||||
.with_top_level_commands(aliases.into_keys());
|
||||
|
||||
let line = "...";
|
||||
|
||||
assert_eq!(
|
||||
describe_at_cursor(line, ByteOffset::from(0), &ctx),
|
||||
Some(Description {
|
||||
token: "...".to_string().spanned(Span::new(0, 3)),
|
||||
description_text: Some("Alias for \"cd ../../\"".to_string()),
|
||||
suggestion_type: SuggestionType::Command(TopLevelCommandCaseSensitivity::CaseSensitive),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn test_xray_describe_functions() {
|
||||
let functions = HashSet::from_iter(["foo".into()]);
|
||||
let ctx = FakeCompletionContext::new(CommandRegistry::default())
|
||||
.with_functions(functions.clone())
|
||||
.with_top_level_commands(functions);
|
||||
|
||||
let line = "foo";
|
||||
|
||||
assert_eq!(
|
||||
describe_at_cursor(line, ByteOffset::from(0), &ctx),
|
||||
Some(Description {
|
||||
token: "foo".to_string().spanned(Span::new(0, 3)),
|
||||
description_text: Some("Shell function".to_string()),
|
||||
suggestion_type: SuggestionType::Command(TopLevelCommandCaseSensitivity::CaseSensitive),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn test_xray_describe_builtins() {
|
||||
let builtins = HashSet::from_iter(["exit".into()]);
|
||||
let ctx = FakeCompletionContext::new(CommandRegistry::default())
|
||||
.with_builtins(builtins.clone())
|
||||
.with_top_level_commands(builtins);
|
||||
|
||||
let line = "exit";
|
||||
|
||||
assert_eq!(
|
||||
describe_at_cursor(line, ByteOffset::from(0), &ctx),
|
||||
Some(Description {
|
||||
token: "exit".to_string().spanned(Span::new(0, 4)),
|
||||
description_text: Some("Shell builtin".to_string()),
|
||||
suggestion_type: SuggestionType::Command(TopLevelCommandCaseSensitivity::CaseSensitive),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn test_xray_describe_abbreviations() {
|
||||
let abbrs = HashMap::from_iter([("ga".into(), "git add".into())]);
|
||||
let ctx = FakeCompletionContext::new(CommandRegistry::default())
|
||||
.with_abbreviations(abbrs.clone())
|
||||
.with_top_level_commands(abbrs.into_keys());
|
||||
|
||||
let line = "ga";
|
||||
|
||||
assert_eq!(
|
||||
describe_at_cursor(line, ByteOffset::from(0), &ctx),
|
||||
Some(Description {
|
||||
token: "ga".to_string().spanned(Span::new(0, 2)),
|
||||
description_text: Some("Abbreviation for \"git add\"".to_string()),
|
||||
suggestion_type: SuggestionType::Command(TopLevelCommandCaseSensitivity::CaseSensitive),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn test_xray_describe_flag_with_equal_sign() {
|
||||
let registry = create_test_command_registry([test_signature()]);
|
||||
let ctx = FakeCompletionContext::new(registry);
|
||||
|
||||
assert_eq!(
|
||||
describe_at_cursor("test --long=foo", ByteOffset::from(7), &ctx),
|
||||
Some(Description {
|
||||
token: "--long".to_string().spanned(Span::new(5, 11)),
|
||||
description_text: None,
|
||||
suggestion_type: SuggestionType::Option(
|
||||
MatchRequirement::EntireName,
|
||||
OptionCaseSensitivity::CaseSensitive,
|
||||
),
|
||||
})
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
describe_at_cursor("test --long=", ByteOffset::from(7), &ctx),
|
||||
Some(Description {
|
||||
token: "--long".to_string().spanned(Span::new(5, 11)),
|
||||
description_text: None,
|
||||
suggestion_type: SuggestionType::Option(
|
||||
MatchRequirement::EntireName,
|
||||
OptionCaseSensitivity::CaseSensitive,
|
||||
),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn test_describe_file_paths_with_separator_in_middle() {
|
||||
let registry = create_test_command_registry([]);
|
||||
let pwd = TypedPathBuf::from(TEST_WORK_DIR);
|
||||
let path_ctx = MockPathCompletionContext::new(pwd.clone())
|
||||
.with_entries_in_pwd([EngineDirEntry::test_dir("foo")])
|
||||
.with_entries(pwd.join("foo/"), [EngineDirEntry::test_file("script")]);
|
||||
let ctx = FakeCompletionContext::new(registry).with_path_completion_context(path_ctx.clone());
|
||||
|
||||
assert_eq!(
|
||||
describe_at_cursor("foo/script", ByteOffset::from(10), &ctx),
|
||||
Some(Description {
|
||||
token: "foo/script".to_string().spanned(Span::new(0, 10)),
|
||||
description_text: Some("File".to_string()),
|
||||
suggestion_type: SuggestionType::Argument
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_describe_powershell_shortened_option() {
|
||||
let registry = create_test_command_registry([add_content_signature(), git_signature()]);
|
||||
let ctx = FakeCompletionContext::new(registry);
|
||||
|
||||
// "-Enc" is specific enough to match "-Encoding" but not "-Exclude"
|
||||
assert_eq!(
|
||||
describe_at_cursor("Add-Content -Enc ASCII", ByteOffset::from(16), &ctx),
|
||||
Some(Description {
|
||||
token: "-Encoding".to_string().spanned(Span::new(12, 16)),
|
||||
description_text: None,
|
||||
suggestion_type: SuggestionType::Option(
|
||||
MatchRequirement::UniquePrefixOnly,
|
||||
OptionCaseSensitivity::CaseInsensitive
|
||||
),
|
||||
})
|
||||
);
|
||||
|
||||
// "-E" is not specific enough, so it shouldn't match.
|
||||
assert_eq!(
|
||||
describe_at_cursor("Add-Content -E ASCII", ByteOffset::from(14), &ctx),
|
||||
None
|
||||
);
|
||||
|
||||
// Shouldn't apply to commands which aren't PowerShell cmdlets
|
||||
assert_eq!(
|
||||
describe_at_cursor("git branch --delet", ByteOffset::from(18), &ctx),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_describe_case_insensitive_option() {
|
||||
let registry = create_test_command_registry([add_content_signature(), git_signature()]);
|
||||
let ctx = FakeCompletionContext::new(registry);
|
||||
|
||||
// "-enc" is specific enough to match "-Encoding" but not "-Exclude"
|
||||
assert_eq!(
|
||||
describe_at_cursor("Add-Content -enc UTF8", ByteOffset::from(16), &ctx),
|
||||
Some(Description {
|
||||
token: "-Encoding".to_string().spanned(Span::new(12, 16)),
|
||||
description_text: None,
|
||||
suggestion_type: SuggestionType::Option(
|
||||
MatchRequirement::UniquePrefixOnly,
|
||||
OptionCaseSensitivity::CaseInsensitive
|
||||
),
|
||||
})
|
||||
);
|
||||
|
||||
// "-e" is not specific enough, so it shouldn't match.
|
||||
assert_eq!(
|
||||
describe_at_cursor("Add-Content -e ASCII", ByteOffset::from(14), &ctx),
|
||||
None
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
//! Contains the legacy implementation of argument suggestion generation that depends on the legacy
|
||||
//! command signature struct (`warp_command_signatures::Signature`).
|
||||
use std::{borrow::Cow, collections::HashMap};
|
||||
use std::borrow::Cow;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use galaxy_core::features::FeatureFlag;
|
||||
use galaxy_util::path::ShellFamily;
|
||||
@@ -11,32 +12,26 @@ use warp_command_signatures::{
|
||||
Template, TemplateFilter, TemplateType,
|
||||
};
|
||||
|
||||
use crate::completer::{
|
||||
context::CompletionContext,
|
||||
engine::{
|
||||
self,
|
||||
path::{sorted_directories_relative_to, sorted_paths_relative_to, EngineFileType},
|
||||
},
|
||||
matchers::MatchStrategy,
|
||||
suggest::{
|
||||
CompleterOptions, CompletionsFallbackStrategy, MatchedSuggestion, Suggestion,
|
||||
SuggestionType,
|
||||
},
|
||||
CommandExitStatus, GeneratorContext, LocationType,
|
||||
use super::add_extra_positional;
|
||||
use crate::completer::context::CompletionContext;
|
||||
use crate::completer::engine::path::{
|
||||
sorted_cd_directories, sorted_directories_relative_to, sorted_paths_relative_to, EngineFileType,
|
||||
};
|
||||
|
||||
use crate::completer::engine::{self};
|
||||
use crate::completer::matchers::MatchStrategy;
|
||||
use crate::completer::suggest::{
|
||||
CompleterOptions, CompletionsFallbackStrategy, MatchedSuggestion, Suggestion, SuggestionType,
|
||||
};
|
||||
use crate::completer::{CommandExitStatus, GeneratorContext, LocationType};
|
||||
use crate::meta::{Span, Spanned};
|
||||
use crate::parsers::hir::{Command, ShellCommand};
|
||||
use crate::parsers::ArgumentError::{
|
||||
MissingMandatoryPositional, MissingValueForName, UnexpectedArgument,
|
||||
};
|
||||
use crate::parsers::{
|
||||
ClassifiedCommand, ParseError, ParseErrorReason, ParsedToken, SignatureAtTokenIndex,
|
||||
};
|
||||
|
||||
use crate::parsers::{
|
||||
hir::{Command, ShellCommand},
|
||||
ArgumentError::{MissingMandatoryPositional, MissingValueForName, UnexpectedArgument},
|
||||
};
|
||||
|
||||
use super::add_extra_positional;
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn complete(
|
||||
line: &str,
|
||||
@@ -154,7 +149,7 @@ async fn suggestions_for_parse_error(
|
||||
missing_arg_index,
|
||||
},
|
||||
} => {
|
||||
// If there was trailing whitespace in the line, respect the error and try to to complete based
|
||||
// If there was trailing whitespace in the line, respect the error and try to complete based
|
||||
// on the missing argument. If there wasn't any trailing whitespace, the user is trying
|
||||
// to complete an argument before the one that's missing (such as `git push ori<tab>`) so we
|
||||
// treat this as successful parse so that we can parse out the argument correctly.
|
||||
@@ -213,7 +208,7 @@ async fn suggestions_for_parse_error(
|
||||
positional_index,
|
||||
},
|
||||
} => {
|
||||
// If there was ending whitespace in the line respect the error and try to to complete based
|
||||
// If there was ending whitespace in the line respect the error and try to complete based
|
||||
// on the missing positional. If there was not an ending whitespace, the user is try trying
|
||||
// to complete a positional before the one that's missing such as `git push ori<tab>` so we
|
||||
// treat this as successful parse so that we can parse out the positional correctly.
|
||||
@@ -303,7 +298,7 @@ async fn suggestions_for_last_argument(
|
||||
add_extra_positional(shell_command, cursor);
|
||||
}
|
||||
|
||||
// Find the last positional and named value within the the command that the user entered.
|
||||
// Find the last positional and named value within the command that the user entered.
|
||||
// Whichever ends last is the value we're trying to complete on.
|
||||
let last_positional = shell_command.last_positional();
|
||||
let last_named_value = shell_command.last_named_argument();
|
||||
@@ -668,10 +663,19 @@ async fn generate_suggestions_for_argument_type(
|
||||
type_name: TemplateType::Folders { .. },
|
||||
filter_name,
|
||||
}) => {
|
||||
let is_cd = tokens_from_command.first().is_some_and(|t| *t == "cd");
|
||||
let path_suggestions = match ctx.path_completion_context() {
|
||||
Some(path_completion_context) => {
|
||||
sorted_directories_relative_to(parsed_token, matcher, path_completion_context)
|
||||
if is_cd {
|
||||
sorted_cd_directories(parsed_token, matcher, path_completion_context).await
|
||||
} else {
|
||||
sorted_directories_relative_to(
|
||||
parsed_token,
|
||||
matcher,
|
||||
path_completion_context,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
None => Vec::new(),
|
||||
};
|
||||
|
||||
@@ -8,13 +8,9 @@ cfg_if::cfg_if! {
|
||||
}
|
||||
}
|
||||
|
||||
use crate::{
|
||||
meta::{Span, SpannedItem},
|
||||
parsers::{
|
||||
hir::{Expression, ShellCommand},
|
||||
ParsedExpression, ParsedToken,
|
||||
},
|
||||
};
|
||||
use crate::meta::{Span, SpannedItem};
|
||||
use crate::parsers::hir::{Expression, ShellCommand};
|
||||
use crate::parsers::{ParsedExpression, ParsedToken};
|
||||
|
||||
/// Creates a new empty positional arg in a shell_command. This is useful before evaluating args
|
||||
/// so that we don't include the extra whitespace at the end of the command (e.g. "cd ") within the
|
||||
|
||||
@@ -15,24 +15,23 @@ use itertools::Itertools;
|
||||
use smol_str::SmolStr;
|
||||
|
||||
use super::add_extra_positional;
|
||||
use crate::completer::GeneratorContext;
|
||||
use crate::{
|
||||
completer::{
|
||||
context::call_js_function,
|
||||
engine::path::{sorted_directories_relative_to, sorted_paths_relative_to},
|
||||
CommandExitStatus, CompleterOptions, CompletionContext, CompletionsFallbackStrategy,
|
||||
LocationType, MatchStrategy, MatchedSuggestion, Suggestion, SuggestionType,
|
||||
},
|
||||
meta::{Span, Spanned},
|
||||
parsers::{
|
||||
hir::{self, ShellCommand},
|
||||
ArgumentError::{MissingMandatoryPositional, MissingValueForName, UnexpectedArgument},
|
||||
ClassifiedCommand, ParseError, ParseErrorReason, ParsedToken,
|
||||
},
|
||||
signatures::{
|
||||
self, Argument, ArgumentValue, Command, GeneratorCompletionContext, GeneratorFn,
|
||||
GeneratorResults, GeneratorScript, TemplateType,
|
||||
},
|
||||
use crate::completer::context::call_js_function;
|
||||
use crate::completer::engine::path::{
|
||||
sorted_cd_directories, sorted_directories_relative_to, sorted_paths_relative_to,
|
||||
};
|
||||
use crate::completer::{
|
||||
CommandExitStatus, CompleterOptions, CompletionContext, CompletionsFallbackStrategy,
|
||||
GeneratorContext, LocationType, MatchStrategy, MatchedSuggestion, Suggestion, SuggestionType,
|
||||
};
|
||||
use crate::meta::{Span, Spanned};
|
||||
use crate::parsers::hir::{self, ShellCommand};
|
||||
use crate::parsers::ArgumentError::{
|
||||
MissingMandatoryPositional, MissingValueForName, UnexpectedArgument,
|
||||
};
|
||||
use crate::parsers::{ClassifiedCommand, ParseError, ParseErrorReason, ParsedToken};
|
||||
use crate::signatures::{
|
||||
self, Argument, ArgumentValue, Command, GeneratorCompletionContext, GeneratorFn,
|
||||
GeneratorResults, GeneratorScript, TemplateType,
|
||||
};
|
||||
|
||||
/// Returns completion suggestions for argument values based on the given `input`.
|
||||
@@ -143,7 +142,7 @@ async fn suggestions_for_parse_error(
|
||||
missing_arg_index,
|
||||
},
|
||||
} => {
|
||||
// If there was trailing whitespace in the line, respect the error and try to to complete based
|
||||
// If there was trailing whitespace in the line, respect the error and try to complete based
|
||||
// on the missing argument. If there wasn't any trailing whitespace, the user is trying
|
||||
// to complete an argument before the one that's missing (such as `git push ori<tab>`) so we
|
||||
// treat this as successful parse so that we can parse out the argument correctly.
|
||||
@@ -196,7 +195,7 @@ async fn suggestions_for_parse_error(
|
||||
positional_index,
|
||||
},
|
||||
} => {
|
||||
// If there was ending whitespace in the line respect the error and try to to complete based
|
||||
// If there was ending whitespace in the line respect the error and try to complete based
|
||||
// on the missing positional. If there was not an ending whitespace, the user is try trying
|
||||
// to complete a positional before the one that's missing such as `git push ori<tab>` so we
|
||||
// treat this as successful parse so that we can parse out the positional correctly.
|
||||
@@ -277,7 +276,7 @@ async fn suggestions_for_last_argument(
|
||||
add_extra_positional(shell_command, cursor);
|
||||
}
|
||||
|
||||
// Find the last positional and named value within the the command that the user entered.
|
||||
// Find the last positional and named value within the command that the user entered.
|
||||
// Whichever ends last is the value we're trying to complete on.
|
||||
let last_positional = shell_command.last_positional();
|
||||
let last_named_value = shell_command.last_named_argument();
|
||||
@@ -581,10 +580,21 @@ async fn generate_suggestions_for_argument_value(
|
||||
type_name: TemplateType::Folders,
|
||||
..
|
||||
} => {
|
||||
let is_cd = tokens_without_last_editing
|
||||
.first()
|
||||
.is_some_and(|t| *t == "cd");
|
||||
let path_suggestions = match ctx.path_completion_context() {
|
||||
Some(path_completion_context) => {
|
||||
sorted_directories_relative_to(parsed_token, matcher, path_completion_context)
|
||||
if is_cd {
|
||||
sorted_cd_directories(parsed_token, matcher, path_completion_context).await
|
||||
} else {
|
||||
sorted_directories_relative_to(
|
||||
parsed_token,
|
||||
matcher,
|
||||
path_completion_context,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
None => Vec::new(),
|
||||
};
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
use itertools::Itertools;
|
||||
|
||||
use crate::completer::{
|
||||
context::CompletionContext,
|
||||
engine, get_path_separators,
|
||||
matchers::MatchStrategy,
|
||||
suggest::{MatchedSuggestion, Priority, Suggestion, SuggestionType},
|
||||
TopLevelCommandCaseSensitivity,
|
||||
};
|
||||
use crate::parsers::ParsedToken;
|
||||
|
||||
use super::path::{sorted_directories_relative_to, sorted_paths_relative_to};
|
||||
use crate::completer::context::CompletionContext;
|
||||
use crate::completer::matchers::MatchStrategy;
|
||||
use crate::completer::suggest::{MatchedSuggestion, Priority, Suggestion, SuggestionType};
|
||||
use crate::completer::{engine, get_path_separators, TopLevelCommandCaseSensitivity};
|
||||
use crate::parsers::ParsedToken;
|
||||
|
||||
/// Generates top-level completion results based on the fragment of text that is entered into the
|
||||
/// buffer. We use the following algorithm to generate suggestions, which is also the same as ZSH:
|
||||
|
||||
@@ -3,12 +3,10 @@
|
||||
use itertools::Itertools;
|
||||
use warp_command_signatures::{FlagStyle, Signature as SpecSignature};
|
||||
|
||||
use crate::completer::{
|
||||
describe::OptionCaseSensitivity,
|
||||
engine::LocationType,
|
||||
matchers::{Match, MatchStrategy},
|
||||
suggest::{MatchRequirement, MatchedSuggestion, Suggestion, SuggestionType},
|
||||
};
|
||||
use crate::completer::describe::OptionCaseSensitivity;
|
||||
use crate::completer::engine::LocationType;
|
||||
use crate::completer::matchers::{Match, MatchStrategy};
|
||||
use crate::completer::suggest::{MatchRequirement, MatchedSuggestion, Suggestion, SuggestionType};
|
||||
use crate::meta::Spanned;
|
||||
use crate::parsers::SignatureAtTokenIndex;
|
||||
|
||||
|
||||
@@ -5,14 +5,13 @@ use std::iter;
|
||||
|
||||
use itertools::Itertools;
|
||||
|
||||
use crate::{
|
||||
completer::{
|
||||
describe::OptionCaseSensitivity, suggest::MatchRequirement, LocationType, Match,
|
||||
MatchStrategy, MatchedSuggestion, Suggestion, SuggestionType,
|
||||
},
|
||||
meta::Spanned,
|
||||
signatures::Command,
|
||||
use crate::completer::describe::OptionCaseSensitivity;
|
||||
use crate::completer::suggest::MatchRequirement;
|
||||
use crate::completer::{
|
||||
LocationType, Match, MatchStrategy, MatchedSuggestion, Suggestion, SuggestionType,
|
||||
};
|
||||
use crate::meta::Spanned;
|
||||
use crate::signatures::Command;
|
||||
|
||||
pub fn complete(
|
||||
matcher: MatchStrategy,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use crate::{completer::TopLevelCommandCaseSensitivity, signatures::CommandRegistry};
|
||||
use crate::completer::TopLevelCommandCaseSensitivity;
|
||||
use crate::signatures::CommandRegistry;
|
||||
|
||||
/// Returns the name of the argument that should be given at `idx` for the given command.
|
||||
pub(super) fn argument_name_at_index_for_command(
|
||||
|
||||
@@ -20,16 +20,13 @@ cfg_if::cfg_if! {
|
||||
}
|
||||
}
|
||||
|
||||
use crate::{
|
||||
completer::{CompletionContext, TopLevelCommandCaseSensitivity},
|
||||
meta::{HasSpan, Span, Spanned, SpannedItem},
|
||||
parsers::{
|
||||
hir::{Command, Expression, ExternalCommand, FlagType, ShellCommand},
|
||||
ArgumentError, ClassifiedCommand, ParseError, ParseErrorReason, ParsedExpression,
|
||||
ParsedToken,
|
||||
},
|
||||
signatures::CommandRegistry,
|
||||
use crate::completer::{CompletionContext, TopLevelCommandCaseSensitivity};
|
||||
use crate::meta::{HasSpan, Span, Spanned, SpannedItem};
|
||||
use crate::parsers::hir::{Command, Expression, ExternalCommand, FlagType, ShellCommand};
|
||||
use crate::parsers::{
|
||||
ArgumentError, ClassifiedCommand, ParseError, ParseErrorReason, ParsedExpression, ParsedToken,
|
||||
};
|
||||
use crate::signatures::CommandRegistry;
|
||||
|
||||
pub type CompletionLocation = Spanned<LocationType>;
|
||||
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
use std::collections::HashSet;
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::fs::DirEntry;
|
||||
use std::sync::Arc;
|
||||
|
||||
use galaxy_util::path::HOME_DIR_ENV_VAR_PREFIX;
|
||||
use async_trait::async_trait;
|
||||
use itertools::{iproduct, Itertools};
|
||||
use lazy_static::lazy_static;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use typed_path::{TypedPath, TypedPathBuf};
|
||||
use warp_command_signatures::{IconType, PathSuggestionType};
|
||||
use galaxy_util::path::{ShellFamily, HOME_DIR_ENV_VAR_PREFIX};
|
||||
|
||||
use crate::completer::suggest::Priority;
|
||||
use crate::completer::{
|
||||
context::PathCompletionContext,
|
||||
matchers::MatchStrategy,
|
||||
suggest::{MatchedSuggestion, Suggestion, SuggestionType},
|
||||
};
|
||||
use crate::completer::context::{PathCompletionContext, PathSeparators};
|
||||
use crate::completer::matchers::MatchStrategy;
|
||||
use crate::completer::suggest::{MatchedSuggestion, Priority, Suggestion, SuggestionType};
|
||||
use crate::parsers::ParsedToken;
|
||||
|
||||
/// TODO(CORE-3074): This only applies to Unix.
|
||||
@@ -136,6 +136,127 @@ pub(crate) async fn sorted_directories_relative_to(
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Like `sorted_directories_relative_to`, but iterates `$CDPATH` in shell
|
||||
/// order (empty/`.` entry = pwd at that position; pwd appended as fallback if
|
||||
/// no such entry) so completions surface in the order `cd` would resolve them.
|
||||
pub(crate) async fn sorted_cd_directories(
|
||||
path: &ParsedToken,
|
||||
matcher: MatchStrategy,
|
||||
ctx: &dyn PathCompletionContext,
|
||||
) -> Vec<MatchedSuggestion> {
|
||||
if !is_cdpath_eligible_token(path.as_str()) {
|
||||
return sorted_directories_relative_to(path, matcher, ctx).await;
|
||||
}
|
||||
|
||||
let Some(cdpath) = ctx.cdpath() else {
|
||||
return sorted_directories_relative_to(path, matcher, ctx).await;
|
||||
};
|
||||
|
||||
let mut results: Vec<MatchedSuggestion> = Vec::new();
|
||||
let mut seen: HashSet<String> = HashSet::new();
|
||||
let mut pwd_searched = false;
|
||||
|
||||
let push_unique = |suggestions: Vec<MatchedSuggestion>,
|
||||
results: &mut Vec<MatchedSuggestion>,
|
||||
seen: &mut HashSet<String>| {
|
||||
for s in suggestions {
|
||||
if seen.insert(s.suggestion.display.to_string()) {
|
||||
results.push(s);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
for entry in cdpath.split(':') {
|
||||
if entry.is_empty() || entry == "." {
|
||||
if pwd_searched {
|
||||
continue;
|
||||
}
|
||||
let pwd = sorted_directories_relative_to(path, matcher, ctx).await;
|
||||
push_unique(pwd, &mut results, &mut seen);
|
||||
pwd_searched = true;
|
||||
} else {
|
||||
let override_ctx = CdpathOverrideContext {
|
||||
inner: ctx,
|
||||
cdpath_pwd: resolve_cdpath_entry(entry, ctx),
|
||||
};
|
||||
let extra = sorted_directories_relative_to(path, matcher, &override_ctx).await;
|
||||
push_unique(extra, &mut results, &mut seen);
|
||||
}
|
||||
}
|
||||
|
||||
// Shell falls back to pwd when no `$CDPATH` entry matches. If no `.`/empty
|
||||
// entry positioned pwd already, append pwd matches now.
|
||||
if !pwd_searched {
|
||||
let pwd = sorted_directories_relative_to(path, matcher, ctx).await;
|
||||
push_unique(pwd, &mut results, &mut seen);
|
||||
}
|
||||
|
||||
results
|
||||
}
|
||||
|
||||
/// Tilde-expand a `$CDPATH` entry against the shell's home dir, then resolve
|
||||
/// relative entries against the shell's pwd so `cd` matches shell behavior.
|
||||
fn resolve_cdpath_entry(entry: &str, ctx: &dyn PathCompletionContext) -> TypedPathBuf {
|
||||
let expanded = if entry == "~" {
|
||||
ctx.home_directory().unwrap_or_default().to_owned()
|
||||
} else if let Some(rest) = entry.strip_prefix("~/") {
|
||||
format!("{}/{}", ctx.home_directory().unwrap_or_default(), rest)
|
||||
} else {
|
||||
entry.to_owned()
|
||||
};
|
||||
|
||||
let resolved = TypedPathBuf::from(expanded.as_str());
|
||||
if resolved.is_absolute() {
|
||||
resolved
|
||||
} else {
|
||||
ctx.pwd().join(expanded)
|
||||
}
|
||||
}
|
||||
|
||||
fn is_cdpath_eligible_token(token: &str) -> bool {
|
||||
!(token.starts_with('/')
|
||||
|| token.starts_with('~')
|
||||
|| token.starts_with("./")
|
||||
|| token.starts_with("../")
|
||||
|| token == "."
|
||||
|| token == "..")
|
||||
}
|
||||
|
||||
/// Wraps a `PathCompletionContext` and overrides only `pwd()` so we can reuse
|
||||
/// the existing engine to list directories under a `$CDPATH` entry.
|
||||
struct CdpathOverrideContext<'a> {
|
||||
inner: &'a dyn PathCompletionContext,
|
||||
cdpath_pwd: TypedPathBuf,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<'a> PathCompletionContext for CdpathOverrideContext<'a> {
|
||||
async fn list_directory_entries(&self, directory: TypedPathBuf) -> Arc<Vec<EngineDirEntry>> {
|
||||
self.inner.list_directory_entries(directory).await
|
||||
}
|
||||
|
||||
fn home_directory(&self) -> Option<&str> {
|
||||
self.inner.home_directory()
|
||||
}
|
||||
|
||||
fn cdpath(&self) -> Option<&str> {
|
||||
// Avoid recursing — the outer call already iterates entries.
|
||||
None
|
||||
}
|
||||
|
||||
fn shell_family(&self) -> ShellFamily {
|
||||
self.inner.shell_family()
|
||||
}
|
||||
|
||||
fn pwd(&self) -> TypedPath<'_> {
|
||||
self.cdpath_pwd.to_path()
|
||||
}
|
||||
|
||||
fn path_separators(&self) -> PathSeparators {
|
||||
self.inner.path_separators()
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn sorted_paths_relative_to(
|
||||
path: &ParsedToken,
|
||||
matcher: MatchStrategy,
|
||||
@@ -307,5 +428,5 @@ impl SplitPath {
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "path_test.rs"]
|
||||
#[path = "path_tests.rs"]
|
||||
mod tests;
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
use warp_command_signatures::IconType;
|
||||
|
||||
use crate::completer::testing::MockPathCompletionContext;
|
||||
|
||||
use super::*;
|
||||
use crate::completer::testing::MockPathCompletionContext;
|
||||
|
||||
#[cfg(windows)]
|
||||
mod windows_constants {
|
||||
@@ -98,7 +97,7 @@ pub fn test_sorted_paths_relative_to() {
|
||||
]);
|
||||
|
||||
assert_eq!(
|
||||
galaxyui::r#async::block_on(sorted_paths_relative_to(
|
||||
galaxyui_core::r#async::block_on(sorted_paths_relative_to(
|
||||
&ParsedToken::empty(),
|
||||
MatchStrategy::CaseInsensitive,
|
||||
&ctx
|
||||
@@ -135,7 +134,7 @@ pub fn test_sorted_paths_relative_to() {
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
galaxyui::r#async::block_on(sorted_paths_relative_to(
|
||||
galaxyui_core::r#async::block_on(sorted_paths_relative_to(
|
||||
&ParsedToken::new("sr"),
|
||||
MatchStrategy::CaseInsensitive,
|
||||
&ctx
|
||||
@@ -154,7 +153,7 @@ pub fn test_sorted_paths_relative_to() {
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
galaxyui::r#async::block_on(sorted_paths_relative_to(
|
||||
galaxyui_core::r#async::block_on(sorted_paths_relative_to(
|
||||
&ParsedToken::new("."),
|
||||
MatchStrategy::CaseInsensitive,
|
||||
&ctx
|
||||
@@ -201,7 +200,7 @@ pub fn test_sorted_directories_relative_to() {
|
||||
]);
|
||||
|
||||
assert_eq!(
|
||||
galaxyui::r#async::block_on(sorted_directories_relative_to(
|
||||
galaxyui_core::r#async::block_on(sorted_directories_relative_to(
|
||||
&ParsedToken::empty(),
|
||||
MatchStrategy::CaseInsensitive,
|
||||
&ctx
|
||||
@@ -230,7 +229,7 @@ pub fn test_sorted_directories_relative_to() {
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
galaxyui::r#async::block_on(sorted_directories_relative_to(
|
||||
galaxyui_core::r#async::block_on(sorted_directories_relative_to(
|
||||
&ParsedToken::new("s"),
|
||||
MatchStrategy::CaseInsensitive,
|
||||
&ctx
|
||||
@@ -264,7 +263,7 @@ pub fn test_sorted_paths_case_insensitive_ordering() {
|
||||
file_entry("cherry.txt"),
|
||||
]);
|
||||
|
||||
let suggestions: Vec<String> = galaxyui::r#async::block_on(sorted_paths_relative_to(
|
||||
let suggestions: Vec<String> = galaxyui_core::r#async::block_on(sorted_paths_relative_to(
|
||||
&ParsedToken::empty(),
|
||||
MatchStrategy::CaseInsensitive,
|
||||
&ctx,
|
||||
@@ -292,7 +291,7 @@ pub fn test_path_completions_with_special_characters_relative_to_cwd() {
|
||||
let ctx = mock_path_completion_ctx_special_characters();
|
||||
|
||||
assert_eq!(
|
||||
galaxyui::r#async::block_on(sorted_directories_relative_to(
|
||||
galaxyui_core::r#async::block_on(sorted_directories_relative_to(
|
||||
&ParsedToken::empty(),
|
||||
MatchStrategy::CaseInsensitive,
|
||||
&ctx
|
||||
@@ -337,7 +336,7 @@ pub fn test_path_completions_with_special_characters_relative_to_cwd() {
|
||||
pub fn test_path_completions_with_special_character_case_insensitive() {
|
||||
let ctx = mock_path_completion_ctx_special_characters();
|
||||
assert_eq!(
|
||||
galaxyui::r#async::block_on(sorted_directories_relative_to(
|
||||
galaxyui_core::r#async::block_on(sorted_directories_relative_to(
|
||||
&ParsedToken::new("~"),
|
||||
MatchStrategy::CaseInsensitive,
|
||||
&ctx
|
||||
@@ -374,7 +373,7 @@ pub fn test_path_completions_with_special_characters_fuzzy() {
|
||||
let ctx = mock_path_completion_ctx_special_characters();
|
||||
|
||||
assert_eq!(
|
||||
galaxyui::r#async::block_on(sorted_directories_relative_to(
|
||||
galaxyui_core::r#async::block_on(sorted_directories_relative_to(
|
||||
&ParsedToken::new("~"),
|
||||
MatchStrategy::Fuzzy,
|
||||
&ctx
|
||||
@@ -427,7 +426,7 @@ pub fn test_path_completions_tilde_expansion() {
|
||||
let ctx = mock_path_completion_ctx_special_characters_home_dir();
|
||||
|
||||
assert_eq!(
|
||||
galaxyui::r#async::block_on(sorted_directories_relative_to(
|
||||
galaxyui_core::r#async::block_on(sorted_directories_relative_to(
|
||||
&ParsedToken::new("~/"),
|
||||
MatchStrategy::Fuzzy,
|
||||
&ctx
|
||||
@@ -453,7 +452,7 @@ pub fn test_path_completions_home_env_var_special_characters() {
|
||||
let ctx = mock_path_completion_ctx_special_characters_home_dir();
|
||||
|
||||
assert_eq!(
|
||||
galaxyui::r#async::block_on(sorted_directories_relative_to(
|
||||
galaxyui_core::r#async::block_on(sorted_directories_relative_to(
|
||||
&ParsedToken::new("$HOME/"),
|
||||
MatchStrategy::Fuzzy,
|
||||
&ctx
|
||||
@@ -472,3 +471,212 @@ pub fn test_path_completions_home_env_var_special_characters() {
|
||||
.with_file_type(EngineFileType::Directory),]
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
pub fn test_sorted_cd_directories_no_cdpath_matches_existing_behavior() {
|
||||
let ctx = MockPathCompletionContext::default()
|
||||
.with_entries_in_pwd([dir_entry("local-only"), dir_entry("shared")]);
|
||||
|
||||
let from_cd = galaxyui_core::r#async::block_on(sorted_cd_directories(
|
||||
&ParsedToken::empty(),
|
||||
MatchStrategy::CaseInsensitive,
|
||||
&ctx,
|
||||
));
|
||||
let from_default = galaxyui_core::r#async::block_on(sorted_directories_relative_to(
|
||||
&ParsedToken::empty(),
|
||||
MatchStrategy::CaseInsensitive,
|
||||
&ctx,
|
||||
));
|
||||
assert_eq!(from_cd, from_default);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
pub fn test_sorted_cd_directories_includes_cdpath_entries() {
|
||||
let ctx = MockPathCompletionContext::default()
|
||||
.with_entries_in_pwd([dir_entry("local-only"), dir_entry("shared")])
|
||||
.with_entries(
|
||||
TypedPathBuf::from("/srv/projects"),
|
||||
[
|
||||
dir_entry("shared"),
|
||||
dir_entry("extra-dir"),
|
||||
file_entry("a-file"),
|
||||
],
|
||||
)
|
||||
.with_cdpath("/srv/projects".to_owned());
|
||||
|
||||
let displays: Vec<String> = galaxyui_core::r#async::block_on(sorted_cd_directories(
|
||||
&ParsedToken::empty(),
|
||||
MatchStrategy::CaseInsensitive,
|
||||
&ctx,
|
||||
))
|
||||
.into_iter()
|
||||
.map(|m| m.suggestion.display.to_string())
|
||||
.collect();
|
||||
|
||||
// CDPATH entries first (in order), pwd appended as fallback. `shared`
|
||||
// appears in both, so the first occurrence (the CDPATH one) wins. Within
|
||||
// each directory, listings are sorted alphabetically.
|
||||
assert_eq!(displays, vec!["extra-dir/", "shared/", "local-only/"]);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
pub fn test_sorted_cd_directories_ignores_cdpath_for_absolute_token() {
|
||||
let ctx = MockPathCompletionContext::default()
|
||||
.with_entries_in_pwd([dir_entry("local-only")])
|
||||
.with_entries(
|
||||
TypedPathBuf::from("/srv/projects"),
|
||||
[dir_entry("extra-dir")],
|
||||
)
|
||||
.with_entries(TypedPathBuf::from("/abs"), [dir_entry("absdir")])
|
||||
.with_cdpath("/srv/projects".to_owned());
|
||||
|
||||
let displays: Vec<String> = galaxyui_core::r#async::block_on(sorted_cd_directories(
|
||||
&ParsedToken::new("/abs/"),
|
||||
MatchStrategy::CaseInsensitive,
|
||||
&ctx,
|
||||
))
|
||||
.into_iter()
|
||||
.map(|m| m.suggestion.display.to_string())
|
||||
.collect();
|
||||
|
||||
assert!(displays.iter().all(|d| d != "extra-dir/"));
|
||||
assert!(displays.contains(&"absdir/".to_owned()));
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
pub fn test_sorted_cd_directories_skips_dot_entry_in_cdpath() {
|
||||
// `.` in CDPATH is handled by the pwd-relative pass; skip it on overlay
|
||||
// to avoid double-listing pwd contents.
|
||||
let ctx = MockPathCompletionContext::default()
|
||||
.with_entries_in_pwd([dir_entry("local-only")])
|
||||
.with_cdpath(".".to_owned());
|
||||
|
||||
let displays: Vec<String> = galaxyui_core::r#async::block_on(sorted_cd_directories(
|
||||
&ParsedToken::empty(),
|
||||
MatchStrategy::CaseInsensitive,
|
||||
&ctx,
|
||||
))
|
||||
.into_iter()
|
||||
.map(|m| m.suggestion.display.to_string())
|
||||
.collect();
|
||||
assert_eq!(displays, vec!["local-only/"]);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
pub fn test_sorted_cd_directories_resolves_relative_cdpath_against_pwd() {
|
||||
// CDPATH=src must resolve to <pwd>/src, not be passed raw.
|
||||
let ctx = MockPathCompletionContext::new(TypedPathBuf::from("/work/proj"))
|
||||
.with_entries_in_pwd([dir_entry("local-only")])
|
||||
.with_entries(
|
||||
TypedPathBuf::from("/work/proj/src"),
|
||||
[dir_entry("inner-mod")],
|
||||
)
|
||||
.with_cdpath("src".to_owned());
|
||||
|
||||
let displays: Vec<String> = galaxyui_core::r#async::block_on(sorted_cd_directories(
|
||||
&ParsedToken::empty(),
|
||||
MatchStrategy::CaseInsensitive,
|
||||
&ctx,
|
||||
))
|
||||
.into_iter()
|
||||
.map(|m| m.suggestion.display.to_string())
|
||||
.collect();
|
||||
assert_eq!(displays, vec!["inner-mod/", "local-only/"]);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
pub fn test_sorted_cd_directories_resolves_parent_relative_cdpath() {
|
||||
// CDPATH=.. must resolve to <pwd>/.. so siblings of pwd are reachable.
|
||||
let ctx = MockPathCompletionContext::new(TypedPathBuf::from("/work/proj"))
|
||||
.with_entries_in_pwd([dir_entry("local-only")])
|
||||
.with_entries(
|
||||
TypedPathBuf::from("/work/proj/.."),
|
||||
[dir_entry("sibling-dir")],
|
||||
)
|
||||
.with_cdpath("..".to_owned());
|
||||
|
||||
let displays: Vec<String> = galaxyui_core::r#async::block_on(sorted_cd_directories(
|
||||
&ParsedToken::empty(),
|
||||
MatchStrategy::CaseInsensitive,
|
||||
&ctx,
|
||||
))
|
||||
.into_iter()
|
||||
.map(|m| m.suggestion.display.to_string())
|
||||
.collect();
|
||||
assert_eq!(displays, vec!["sibling-dir/", "local-only/"]);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
pub fn test_sorted_cd_directories_expands_tilde_in_cdpath() {
|
||||
// Tilde-prefixed CDPATH=~/code must expand to the shell's home dir.
|
||||
let ctx = MockPathCompletionContext::new(TypedPathBuf::from("/work/proj"))
|
||||
.with_home_directory("/home/me".to_owned())
|
||||
.with_entries_in_pwd([dir_entry("local-only")])
|
||||
.with_entries(
|
||||
TypedPathBuf::from("/home/me/code"),
|
||||
[dir_entry("from-home")],
|
||||
)
|
||||
.with_cdpath("~/code".to_owned());
|
||||
|
||||
let displays: Vec<String> = galaxyui_core::r#async::block_on(sorted_cd_directories(
|
||||
&ParsedToken::empty(),
|
||||
MatchStrategy::CaseInsensitive,
|
||||
&ctx,
|
||||
))
|
||||
.into_iter()
|
||||
.map(|m| m.suggestion.display.to_string())
|
||||
.collect();
|
||||
assert_eq!(displays, vec!["from-home/", "local-only/"]);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
pub fn test_sorted_cd_directories_pwd_at_dot_position_is_first() {
|
||||
// CDPATH=":/srv/projects": the empty leading entry means pwd is searched
|
||||
// before /srv/projects, matching shell semantics.
|
||||
let ctx = MockPathCompletionContext::new(TypedPathBuf::from("/work/proj"))
|
||||
.with_entries_in_pwd([dir_entry("local-only"), dir_entry("shared")])
|
||||
.with_entries(
|
||||
TypedPathBuf::from("/srv/projects"),
|
||||
[dir_entry("shared"), dir_entry("extra-dir")],
|
||||
)
|
||||
.with_cdpath(":/srv/projects".to_owned());
|
||||
|
||||
let displays: Vec<String> = galaxyui_core::r#async::block_on(sorted_cd_directories(
|
||||
&ParsedToken::empty(),
|
||||
MatchStrategy::CaseInsensitive,
|
||||
&ctx,
|
||||
))
|
||||
.into_iter()
|
||||
.map(|m| m.suggestion.display.to_string())
|
||||
.collect();
|
||||
assert_eq!(displays, vec!["local-only/", "shared/", "extra-dir/"]);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
pub fn test_sorted_cd_directories_pwd_at_dot_in_middle() {
|
||||
// CDPATH="/srv/a:.:/srv/b": pwd appears between the two CDPATH entries.
|
||||
let ctx = MockPathCompletionContext::new(TypedPathBuf::from("/work/proj"))
|
||||
.with_entries_in_pwd([dir_entry("from-pwd")])
|
||||
.with_entries(TypedPathBuf::from("/srv/a"), [dir_entry("from-a")])
|
||||
.with_entries(TypedPathBuf::from("/srv/b"), [dir_entry("from-b")])
|
||||
.with_cdpath("/srv/a:.:/srv/b".to_owned());
|
||||
|
||||
let displays: Vec<String> = galaxyui_core::r#async::block_on(sorted_cd_directories(
|
||||
&ParsedToken::empty(),
|
||||
MatchStrategy::CaseInsensitive,
|
||||
&ctx,
|
||||
))
|
||||
.into_iter()
|
||||
.map(|m| m.suggestion.display.to_string())
|
||||
.collect();
|
||||
assert_eq!(displays, vec!["from-a/", "from-pwd/", "from-b/"]);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,682 @@
|
||||
use warp_command_signatures::IconType;
|
||||
|
||||
use super::*;
|
||||
use crate::completer::testing::MockPathCompletionContext;
|
||||
|
||||
#[cfg(windows)]
|
||||
mod windows_constants {
|
||||
pub(super) const TEST_HOME_DIR: &str = r"C:\Users\test";
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
use windows_constants::*;
|
||||
|
||||
#[cfg(unix)]
|
||||
mod unix_constants {
|
||||
pub(super) const TEST_HOME_DIR: &str = "/users/test";
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
use unix_constants::*;
|
||||
|
||||
#[test]
|
||||
fn test_split_path() {
|
||||
let path = TypedPathBuf::from_unix("/Users/warpuser");
|
||||
let split_path = SplitPath::new(
|
||||
path.to_path(),
|
||||
"~/Warp.app",
|
||||
Some("/Users/warpuser"),
|
||||
&['/'],
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
split_path,
|
||||
SplitPath {
|
||||
directory_absolute_path: path.clone(),
|
||||
directory_relative_path_name: "~/".to_owned(),
|
||||
file_name: "Warp.app".to_owned()
|
||||
}
|
||||
);
|
||||
|
||||
let split_path = SplitPath::new(
|
||||
path.to_path(),
|
||||
"Warp.app/Contents",
|
||||
Some("/Users/warpuser"),
|
||||
&['/'],
|
||||
);
|
||||
assert_eq!(
|
||||
split_path,
|
||||
SplitPath {
|
||||
directory_absolute_path: TypedPathBuf::from("/Users/warpuser/Warp.app/"),
|
||||
directory_relative_path_name: "Warp.app/".to_owned(),
|
||||
file_name: "Contents".to_owned()
|
||||
}
|
||||
);
|
||||
|
||||
let split_path = SplitPath::new(
|
||||
path.to_path(),
|
||||
"Warp.app/macOS/bin/warp.o",
|
||||
Some("/Users/warpuser"),
|
||||
&['/'],
|
||||
);
|
||||
assert_eq!(
|
||||
split_path,
|
||||
SplitPath {
|
||||
directory_absolute_path: TypedPathBuf::from("/Users/warpuser/Warp.app/macOS/bin/"),
|
||||
directory_relative_path_name: "Warp.app/macOS/bin/".to_owned(),
|
||||
file_name: "warp.o".to_owned()
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
fn file_entry(file_name: &str) -> EngineDirEntry {
|
||||
EngineDirEntry {
|
||||
file_name: file_name.to_owned(),
|
||||
file_type: EngineFileType::File,
|
||||
}
|
||||
}
|
||||
|
||||
fn dir_entry(file_name: &str) -> EngineDirEntry {
|
||||
EngineDirEntry {
|
||||
file_name: file_name.to_owned(),
|
||||
file_type: EngineFileType::Directory,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(
|
||||
windows,
|
||||
ignore = "CORE-3696: path sorting comparison function needs separators"
|
||||
)]
|
||||
#[test]
|
||||
pub fn test_sorted_paths_relative_to() {
|
||||
let ctx = MockPathCompletionContext::default().with_entries_in_pwd([
|
||||
file_entry("Cargo.toml"),
|
||||
dir_entry("src"),
|
||||
dir_entry("target"),
|
||||
dir_entry(".hidden"),
|
||||
]);
|
||||
|
||||
assert_eq!(
|
||||
galaxyui_core::r#async::block_on(sorted_paths_relative_to(
|
||||
&ParsedToken::empty(),
|
||||
MatchStrategy::CaseInsensitive,
|
||||
&ctx
|
||||
))
|
||||
.into_iter()
|
||||
.map(|matched_suggestion| matched_suggestion.suggestion)
|
||||
.collect_vec(),
|
||||
vec![
|
||||
Suggestion::with_same_display_and_replacement(
|
||||
"Cargo.toml",
|
||||
Some("File".into()),
|
||||
SuggestionType::Argument,
|
||||
Priority::default(),
|
||||
)
|
||||
.with_icon_override(IconType::File)
|
||||
.with_file_type(EngineFileType::File),
|
||||
Suggestion::with_same_display_and_replacement(
|
||||
"src/",
|
||||
Some("Directory".into()),
|
||||
SuggestionType::Argument,
|
||||
Priority::default(),
|
||||
)
|
||||
.with_icon_override(IconType::Folder)
|
||||
.with_file_type(EngineFileType::Directory),
|
||||
Suggestion::with_same_display_and_replacement(
|
||||
"target/",
|
||||
Some("Directory".into()),
|
||||
SuggestionType::Argument,
|
||||
Priority::default(),
|
||||
)
|
||||
.with_icon_override(IconType::Folder)
|
||||
.with_file_type(EngineFileType::Directory),
|
||||
]
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
galaxyui_core::r#async::block_on(sorted_paths_relative_to(
|
||||
&ParsedToken::new("sr"),
|
||||
MatchStrategy::CaseInsensitive,
|
||||
&ctx
|
||||
))
|
||||
.into_iter()
|
||||
.map(|matched_suggestion| matched_suggestion.suggestion)
|
||||
.collect_vec(),
|
||||
vec![Suggestion::with_same_display_and_replacement(
|
||||
"src/",
|
||||
Some("Directory".into()),
|
||||
SuggestionType::Argument,
|
||||
Priority::default(),
|
||||
)
|
||||
.with_icon_override(IconType::Folder)
|
||||
.with_file_type(EngineFileType::Directory)]
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
galaxyui_core::r#async::block_on(sorted_paths_relative_to(
|
||||
&ParsedToken::new("."),
|
||||
MatchStrategy::CaseInsensitive,
|
||||
&ctx
|
||||
))
|
||||
.into_iter()
|
||||
.map(|matched_suggestion| matched_suggestion.suggestion)
|
||||
.collect_vec(),
|
||||
vec![
|
||||
Suggestion::with_same_display_and_replacement(
|
||||
"./",
|
||||
Some("Directory".into()),
|
||||
SuggestionType::Argument,
|
||||
Priority::default(),
|
||||
)
|
||||
.with_icon_override(IconType::Folder)
|
||||
.with_file_type(EngineFileType::Directory),
|
||||
Suggestion::with_same_display_and_replacement(
|
||||
"../",
|
||||
Some("Directory".into()),
|
||||
SuggestionType::Argument,
|
||||
Priority::default(),
|
||||
)
|
||||
.with_icon_override(IconType::Folder)
|
||||
.with_file_type(EngineFileType::Directory),
|
||||
Suggestion::with_same_display_and_replacement(
|
||||
".hidden/",
|
||||
Some("Directory".into()),
|
||||
SuggestionType::Argument,
|
||||
Priority::default(),
|
||||
)
|
||||
.with_icon_override(IconType::Folder)
|
||||
.with_file_type(EngineFileType::Directory),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn test_sorted_directories_relative_to() {
|
||||
let ctx = MockPathCompletionContext::default().with_entries_in_pwd([
|
||||
file_entry("Cargo.toml"),
|
||||
dir_entry("src"),
|
||||
dir_entry("target"),
|
||||
dir_entry(".hidden"),
|
||||
]);
|
||||
|
||||
assert_eq!(
|
||||
galaxyui_core::r#async::block_on(sorted_directories_relative_to(
|
||||
&ParsedToken::empty(),
|
||||
MatchStrategy::CaseInsensitive,
|
||||
&ctx
|
||||
))
|
||||
.into_iter()
|
||||
.map(|matched_suggestion| matched_suggestion.suggestion)
|
||||
.collect_vec(),
|
||||
vec![
|
||||
Suggestion::with_same_display_and_replacement(
|
||||
"src/",
|
||||
Some("Directory".into()),
|
||||
SuggestionType::Argument,
|
||||
Priority::default(),
|
||||
)
|
||||
.with_icon_override(IconType::Folder)
|
||||
.with_file_type(EngineFileType::Directory),
|
||||
Suggestion::with_same_display_and_replacement(
|
||||
"target/",
|
||||
Some("Directory".into()),
|
||||
SuggestionType::Argument,
|
||||
Priority::default(),
|
||||
)
|
||||
.with_icon_override(IconType::Folder)
|
||||
.with_file_type(EngineFileType::Directory),
|
||||
]
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
galaxyui_core::r#async::block_on(sorted_directories_relative_to(
|
||||
&ParsedToken::new("s"),
|
||||
MatchStrategy::CaseInsensitive,
|
||||
&ctx
|
||||
))
|
||||
.into_iter()
|
||||
.map(|matched_suggestion| matched_suggestion.suggestion)
|
||||
.collect_vec(),
|
||||
vec![Suggestion::with_same_display_and_replacement(
|
||||
"src/",
|
||||
Some("Directory".into()),
|
||||
SuggestionType::Argument,
|
||||
Priority::default(),
|
||||
)
|
||||
.with_icon_override(IconType::Folder)
|
||||
.with_file_type(EngineFileType::Directory)]
|
||||
);
|
||||
}
|
||||
|
||||
/// Verify that path suggestions are sorted case-insensitively so that uppercase entries
|
||||
/// don't always appear before lowercase ones.
|
||||
#[cfg_attr(
|
||||
windows,
|
||||
ignore = "CORE-3696: path sorting comparison function needs separators"
|
||||
)]
|
||||
#[test]
|
||||
pub fn test_sorted_paths_case_insensitive_ordering() {
|
||||
let ctx = MockPathCompletionContext::default().with_entries_in_pwd([
|
||||
file_entry("Zebra.txt"),
|
||||
file_entry("apple.txt"),
|
||||
dir_entry("Banana"),
|
||||
file_entry("cherry.txt"),
|
||||
]);
|
||||
|
||||
let suggestions: Vec<String> = galaxyui_core::r#async::block_on(sorted_paths_relative_to(
|
||||
&ParsedToken::empty(),
|
||||
MatchStrategy::CaseInsensitive,
|
||||
&ctx,
|
||||
))
|
||||
.into_iter()
|
||||
.map(|matched_suggestion| matched_suggestion.suggestion.display.to_string())
|
||||
.collect();
|
||||
|
||||
// Expected case-insensitive order: apple, Banana, cherry, Zebra
|
||||
assert_eq!(
|
||||
suggestions,
|
||||
vec!["apple.txt", "Banana/", "cherry.txt", "Zebra.txt"]
|
||||
);
|
||||
}
|
||||
|
||||
fn mock_path_completion_ctx_special_characters() -> MockPathCompletionContext {
|
||||
MockPathCompletionContext::default()
|
||||
.with_home_directory(TEST_HOME_DIR.to_owned())
|
||||
.with_entries_in_pwd([dir_entry("!nice ~"), dir_entry("~"), dir_entry("~foo")])
|
||||
}
|
||||
|
||||
/// Check that special characters are properly escaped in the Suggestion.
|
||||
#[test]
|
||||
pub fn test_path_completions_with_special_characters_relative_to_cwd() {
|
||||
let ctx = mock_path_completion_ctx_special_characters();
|
||||
|
||||
assert_eq!(
|
||||
galaxyui_core::r#async::block_on(sorted_directories_relative_to(
|
||||
&ParsedToken::empty(),
|
||||
MatchStrategy::CaseInsensitive,
|
||||
&ctx
|
||||
))
|
||||
.into_iter()
|
||||
.map(|matched_suggestion| matched_suggestion.suggestion)
|
||||
.collect_vec(),
|
||||
vec![
|
||||
Suggestion::new(
|
||||
"!nice ~/",
|
||||
r"\!nice\ \~/",
|
||||
Some("Directory".into()),
|
||||
SuggestionType::Argument,
|
||||
Priority::default(),
|
||||
)
|
||||
.with_icon_override(IconType::Folder)
|
||||
.with_file_type(EngineFileType::Directory),
|
||||
Suggestion::new(
|
||||
"~/",
|
||||
r"\~/",
|
||||
Some("Directory".into()),
|
||||
SuggestionType::Argument,
|
||||
Priority::default(),
|
||||
)
|
||||
.with_icon_override(IconType::Folder)
|
||||
.with_file_type(EngineFileType::Directory),
|
||||
Suggestion::new(
|
||||
"~foo/",
|
||||
r"\~foo/",
|
||||
Some("Directory".into()),
|
||||
SuggestionType::Argument,
|
||||
Priority::default(),
|
||||
)
|
||||
.with_icon_override(IconType::Folder)
|
||||
.with_file_type(EngineFileType::Directory),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/// Check that we can match on special characters at the beginning of the file name.
|
||||
#[test]
|
||||
pub fn test_path_completions_with_special_character_case_insensitive() {
|
||||
let ctx = mock_path_completion_ctx_special_characters();
|
||||
assert_eq!(
|
||||
galaxyui_core::r#async::block_on(sorted_directories_relative_to(
|
||||
&ParsedToken::new("~"),
|
||||
MatchStrategy::CaseInsensitive,
|
||||
&ctx
|
||||
))
|
||||
.into_iter()
|
||||
.map(|matched_suggestion| matched_suggestion.suggestion)
|
||||
.collect_vec(),
|
||||
vec![
|
||||
Suggestion::new(
|
||||
"~/",
|
||||
r"\~/",
|
||||
Some("Directory".into()),
|
||||
SuggestionType::Argument,
|
||||
Priority::default(),
|
||||
)
|
||||
.with_icon_override(IconType::Folder)
|
||||
.with_file_type(EngineFileType::Directory),
|
||||
Suggestion::new(
|
||||
"~foo/",
|
||||
r"\~foo/",
|
||||
Some("Directory".into()),
|
||||
SuggestionType::Argument,
|
||||
Priority::default(),
|
||||
)
|
||||
.with_icon_override(IconType::Folder)
|
||||
.with_file_type(EngineFileType::Directory),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/// Check that we can match on special characters regardless of their position in the file name.
|
||||
#[test]
|
||||
pub fn test_path_completions_with_special_characters_fuzzy() {
|
||||
let ctx = mock_path_completion_ctx_special_characters();
|
||||
|
||||
assert_eq!(
|
||||
galaxyui_core::r#async::block_on(sorted_directories_relative_to(
|
||||
&ParsedToken::new("~"),
|
||||
MatchStrategy::Fuzzy,
|
||||
&ctx
|
||||
))
|
||||
.into_iter()
|
||||
.map(|matched_suggestion| matched_suggestion.suggestion)
|
||||
.collect_vec(),
|
||||
vec![
|
||||
Suggestion::new(
|
||||
"!nice ~/",
|
||||
r"\!nice\ \~/",
|
||||
Some("Directory".into()),
|
||||
SuggestionType::Argument,
|
||||
Priority::default(),
|
||||
)
|
||||
.with_icon_override(IconType::Folder)
|
||||
.with_file_type(EngineFileType::Directory),
|
||||
Suggestion::new(
|
||||
"~/",
|
||||
r"\~/",
|
||||
Some("Directory".into()),
|
||||
SuggestionType::Argument,
|
||||
Priority::default(),
|
||||
)
|
||||
.with_icon_override(IconType::Folder)
|
||||
.with_file_type(EngineFileType::Directory),
|
||||
Suggestion::new(
|
||||
"~foo/",
|
||||
r"\~foo/",
|
||||
Some("Directory".into()),
|
||||
SuggestionType::Argument,
|
||||
Priority::default(),
|
||||
)
|
||||
.with_icon_override(IconType::Folder)
|
||||
.with_file_type(EngineFileType::Directory),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
fn mock_path_completion_ctx_special_characters_home_dir() -> MockPathCompletionContext {
|
||||
MockPathCompletionContext::default()
|
||||
.with_home_directory(TEST_HOME_DIR.to_owned())
|
||||
.with_entries_in_pwd([dir_entry("~")])
|
||||
.with_entries(TEST_HOME_DIR.into(), [dir_entry(r"~ testdir")])
|
||||
}
|
||||
|
||||
/// Check that tilde expansion works with path completion and special characters in Suggestions.
|
||||
#[test]
|
||||
pub fn test_path_completions_tilde_expansion() {
|
||||
let ctx = mock_path_completion_ctx_special_characters_home_dir();
|
||||
|
||||
assert_eq!(
|
||||
galaxyui_core::r#async::block_on(sorted_directories_relative_to(
|
||||
&ParsedToken::new("~/"),
|
||||
MatchStrategy::Fuzzy,
|
||||
&ctx
|
||||
))
|
||||
.into_iter()
|
||||
.map(|matched_suggestion| matched_suggestion.suggestion)
|
||||
.collect_vec(),
|
||||
vec![Suggestion::new(
|
||||
"~ testdir/",
|
||||
r"~/\~\ testdir/",
|
||||
Some("Directory".into()),
|
||||
SuggestionType::Argument,
|
||||
Priority::default(),
|
||||
)
|
||||
.with_icon_override(IconType::Folder)
|
||||
.with_file_type(EngineFileType::Directory),]
|
||||
);
|
||||
}
|
||||
|
||||
/// Check that $HOME home directory expansion works with special characters in the suggestions.
|
||||
#[test]
|
||||
pub fn test_path_completions_home_env_var_special_characters() {
|
||||
let ctx = mock_path_completion_ctx_special_characters_home_dir();
|
||||
|
||||
assert_eq!(
|
||||
galaxyui_core::r#async::block_on(sorted_directories_relative_to(
|
||||
&ParsedToken::new("$HOME/"),
|
||||
MatchStrategy::Fuzzy,
|
||||
&ctx
|
||||
))
|
||||
.into_iter()
|
||||
.map(|matched_suggestion| matched_suggestion.suggestion)
|
||||
.collect_vec(),
|
||||
vec![Suggestion::new(
|
||||
"~ testdir/",
|
||||
r"$HOME/\~\ testdir/",
|
||||
Some("Directory".into()),
|
||||
SuggestionType::Argument,
|
||||
Priority::default(),
|
||||
)
|
||||
.with_icon_override(IconType::Folder)
|
||||
.with_file_type(EngineFileType::Directory),]
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
pub fn test_sorted_cd_directories_no_cdpath_matches_existing_behavior() {
|
||||
let ctx = MockPathCompletionContext::default()
|
||||
.with_entries_in_pwd([dir_entry("local-only"), dir_entry("shared")]);
|
||||
|
||||
let from_cd = galaxyui_core::r#async::block_on(sorted_cd_directories(
|
||||
&ParsedToken::empty(),
|
||||
MatchStrategy::CaseInsensitive,
|
||||
&ctx,
|
||||
));
|
||||
let from_default = galaxyui_core::r#async::block_on(sorted_directories_relative_to(
|
||||
&ParsedToken::empty(),
|
||||
MatchStrategy::CaseInsensitive,
|
||||
&ctx,
|
||||
));
|
||||
assert_eq!(from_cd, from_default);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
pub fn test_sorted_cd_directories_includes_cdpath_entries() {
|
||||
let ctx = MockPathCompletionContext::default()
|
||||
.with_entries_in_pwd([dir_entry("local-only"), dir_entry("shared")])
|
||||
.with_entries(
|
||||
TypedPathBuf::from("/srv/projects"),
|
||||
[
|
||||
dir_entry("shared"),
|
||||
dir_entry("extra-dir"),
|
||||
file_entry("a-file"),
|
||||
],
|
||||
)
|
||||
.with_cdpath("/srv/projects".to_owned());
|
||||
|
||||
let displays: Vec<String> = galaxyui_core::r#async::block_on(sorted_cd_directories(
|
||||
&ParsedToken::empty(),
|
||||
MatchStrategy::CaseInsensitive,
|
||||
&ctx,
|
||||
))
|
||||
.into_iter()
|
||||
.map(|m| m.suggestion.display.to_string())
|
||||
.collect();
|
||||
|
||||
// CDPATH entries first (in order), pwd appended as fallback. `shared`
|
||||
// appears in both, so the first occurrence (the CDPATH one) wins. Within
|
||||
// each directory, listings are sorted alphabetically.
|
||||
assert_eq!(displays, vec!["extra-dir/", "shared/", "local-only/"]);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
pub fn test_sorted_cd_directories_ignores_cdpath_for_absolute_token() {
|
||||
let ctx = MockPathCompletionContext::default()
|
||||
.with_entries_in_pwd([dir_entry("local-only")])
|
||||
.with_entries(
|
||||
TypedPathBuf::from("/srv/projects"),
|
||||
[dir_entry("extra-dir")],
|
||||
)
|
||||
.with_entries(TypedPathBuf::from("/abs"), [dir_entry("absdir")])
|
||||
.with_cdpath("/srv/projects".to_owned());
|
||||
|
||||
let displays: Vec<String> = galaxyui_core::r#async::block_on(sorted_cd_directories(
|
||||
&ParsedToken::new("/abs/"),
|
||||
MatchStrategy::CaseInsensitive,
|
||||
&ctx,
|
||||
))
|
||||
.into_iter()
|
||||
.map(|m| m.suggestion.display.to_string())
|
||||
.collect();
|
||||
|
||||
assert!(displays.iter().all(|d| d != "extra-dir/"));
|
||||
assert!(displays.contains(&"absdir/".to_owned()));
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
pub fn test_sorted_cd_directories_skips_dot_entry_in_cdpath() {
|
||||
// `.` in CDPATH is handled by the pwd-relative pass; skip it on overlay
|
||||
// to avoid double-listing pwd contents.
|
||||
let ctx = MockPathCompletionContext::default()
|
||||
.with_entries_in_pwd([dir_entry("local-only")])
|
||||
.with_cdpath(".".to_owned());
|
||||
|
||||
let displays: Vec<String> = galaxyui_core::r#async::block_on(sorted_cd_directories(
|
||||
&ParsedToken::empty(),
|
||||
MatchStrategy::CaseInsensitive,
|
||||
&ctx,
|
||||
))
|
||||
.into_iter()
|
||||
.map(|m| m.suggestion.display.to_string())
|
||||
.collect();
|
||||
assert_eq!(displays, vec!["local-only/"]);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
pub fn test_sorted_cd_directories_resolves_relative_cdpath_against_pwd() {
|
||||
// CDPATH=src must resolve to <pwd>/src, not be passed raw.
|
||||
let ctx = MockPathCompletionContext::new(TypedPathBuf::from("/work/proj"))
|
||||
.with_entries_in_pwd([dir_entry("local-only")])
|
||||
.with_entries(
|
||||
TypedPathBuf::from("/work/proj/src"),
|
||||
[dir_entry("inner-mod")],
|
||||
)
|
||||
.with_cdpath("src".to_owned());
|
||||
|
||||
let displays: Vec<String> = galaxyui_core::r#async::block_on(sorted_cd_directories(
|
||||
&ParsedToken::empty(),
|
||||
MatchStrategy::CaseInsensitive,
|
||||
&ctx,
|
||||
))
|
||||
.into_iter()
|
||||
.map(|m| m.suggestion.display.to_string())
|
||||
.collect();
|
||||
assert_eq!(displays, vec!["inner-mod/", "local-only/"]);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
pub fn test_sorted_cd_directories_resolves_parent_relative_cdpath() {
|
||||
// CDPATH=.. must resolve to <pwd>/.. so siblings of pwd are reachable.
|
||||
let ctx = MockPathCompletionContext::new(TypedPathBuf::from("/work/proj"))
|
||||
.with_entries_in_pwd([dir_entry("local-only")])
|
||||
.with_entries(
|
||||
TypedPathBuf::from("/work/proj/.."),
|
||||
[dir_entry("sibling-dir")],
|
||||
)
|
||||
.with_cdpath("..".to_owned());
|
||||
|
||||
let displays: Vec<String> = galaxyui_core::r#async::block_on(sorted_cd_directories(
|
||||
&ParsedToken::empty(),
|
||||
MatchStrategy::CaseInsensitive,
|
||||
&ctx,
|
||||
))
|
||||
.into_iter()
|
||||
.map(|m| m.suggestion.display.to_string())
|
||||
.collect();
|
||||
assert_eq!(displays, vec!["sibling-dir/", "local-only/"]);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
pub fn test_sorted_cd_directories_expands_tilde_in_cdpath() {
|
||||
// Tilde-prefixed CDPATH=~/code must expand to the shell's home dir.
|
||||
let ctx = MockPathCompletionContext::new(TypedPathBuf::from("/work/proj"))
|
||||
.with_home_directory("/home/me".to_owned())
|
||||
.with_entries_in_pwd([dir_entry("local-only")])
|
||||
.with_entries(
|
||||
TypedPathBuf::from("/home/me/code"),
|
||||
[dir_entry("from-home")],
|
||||
)
|
||||
.with_cdpath("~/code".to_owned());
|
||||
|
||||
let displays: Vec<String> = galaxyui_core::r#async::block_on(sorted_cd_directories(
|
||||
&ParsedToken::empty(),
|
||||
MatchStrategy::CaseInsensitive,
|
||||
&ctx,
|
||||
))
|
||||
.into_iter()
|
||||
.map(|m| m.suggestion.display.to_string())
|
||||
.collect();
|
||||
assert_eq!(displays, vec!["from-home/", "local-only/"]);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
pub fn test_sorted_cd_directories_pwd_at_dot_position_is_first() {
|
||||
// CDPATH=":/srv/projects": the empty leading entry means pwd is searched
|
||||
// before /srv/projects, matching shell semantics.
|
||||
let ctx = MockPathCompletionContext::new(TypedPathBuf::from("/work/proj"))
|
||||
.with_entries_in_pwd([dir_entry("local-only"), dir_entry("shared")])
|
||||
.with_entries(
|
||||
TypedPathBuf::from("/srv/projects"),
|
||||
[dir_entry("shared"), dir_entry("extra-dir")],
|
||||
)
|
||||
.with_cdpath(":/srv/projects".to_owned());
|
||||
|
||||
let displays: Vec<String> = galaxyui_core::r#async::block_on(sorted_cd_directories(
|
||||
&ParsedToken::empty(),
|
||||
MatchStrategy::CaseInsensitive,
|
||||
&ctx,
|
||||
))
|
||||
.into_iter()
|
||||
.map(|m| m.suggestion.display.to_string())
|
||||
.collect();
|
||||
assert_eq!(displays, vec!["local-only/", "shared/", "extra-dir/"]);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
pub fn test_sorted_cd_directories_pwd_at_dot_in_middle() {
|
||||
// CDPATH="/srv/a:.:/srv/b": pwd appears between the two CDPATH entries.
|
||||
let ctx = MockPathCompletionContext::new(TypedPathBuf::from("/work/proj"))
|
||||
.with_entries_in_pwd([dir_entry("from-pwd")])
|
||||
.with_entries(TypedPathBuf::from("/srv/a"), [dir_entry("from-a")])
|
||||
.with_entries(TypedPathBuf::from("/srv/b"), [dir_entry("from-b")])
|
||||
.with_cdpath("/srv/a:.:/srv/b".to_owned());
|
||||
|
||||
let displays: Vec<String> = galaxyui_core::r#async::block_on(sorted_cd_directories(
|
||||
&ParsedToken::empty(),
|
||||
MatchStrategy::CaseInsensitive,
|
||||
&ctx,
|
||||
))
|
||||
.into_iter()
|
||||
.map(|m| m.suggestion.display.to_string())
|
||||
.collect();
|
||||
assert_eq!(displays, vec!["from-a/", "from-pwd/", "from-b/"]);
|
||||
}
|
||||
@@ -1,16 +1,15 @@
|
||||
use galaxy_util::path::EscapeChar;
|
||||
use itertools::Itertools;
|
||||
use string_offset::ByteOffset;
|
||||
|
||||
use super::LocationType;
|
||||
use crate::completer::testing::FakeCompletionContext;
|
||||
use crate::completer::CompletionContext;
|
||||
use crate::meta::{Span, SpannedItem};
|
||||
use crate::parsers::simple::command_at_cursor_position;
|
||||
use crate::parsers::ParsedToken;
|
||||
use crate::parsers::{classify_command, simple::parse_for_completions};
|
||||
use crate::parsers::simple::{command_at_cursor_position, parse_for_completions};
|
||||
use crate::parsers::{classify_command, ParsedToken};
|
||||
use crate::signatures::testing::{create_test_command_registry, test_signature};
|
||||
use crate::signatures::CommandRegistry;
|
||||
use string_offset::ByteOffset;
|
||||
|
||||
fn location(line: &str, registry: CommandRegistry, pos: usize) -> Vec<LocationType> {
|
||||
let ctx = FakeCompletionContext::new(registry);
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
use crate::{
|
||||
completer::TopLevelCommandCaseSensitivity,
|
||||
signatures::{get_matching_signature_for_input, CommandRegistry},
|
||||
};
|
||||
use crate::completer::TopLevelCommandCaseSensitivity;
|
||||
use crate::signatures::{get_matching_signature_for_input, CommandRegistry};
|
||||
|
||||
/// Returns the name of the argument that should be given at `idx` for the given command.
|
||||
pub(super) fn argument_name_at_index_for_command(
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use crate::completer::{
|
||||
matchers::MatchStrategy,
|
||||
suggest::{MatchedSuggestion, Priority, Suggestion, SuggestionType},
|
||||
};
|
||||
|
||||
use crate::parsers::ParsedToken;
|
||||
use itertools::Itertools;
|
||||
use smol_str::SmolStr;
|
||||
|
||||
use crate::completer::matchers::MatchStrategy;
|
||||
use crate::completer::suggest::{MatchedSuggestion, Priority, Suggestion, SuggestionType};
|
||||
use crate::parsers::ParsedToken;
|
||||
|
||||
pub fn suggestions(
|
||||
matcher: MatchStrategy,
|
||||
env_vars: &HashSet<SmolStr>,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use fuzzy_match::{match_indices_case_insensitive, FuzzyMatchResult};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Determine if `from` starts with `partial` in a case insensitive manner.
|
||||
/// Returns None if `partial` does not start with `from`, otherwise specifying
|
||||
@@ -110,5 +109,5 @@ impl From<Match> for MatchType {
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "matchers_test.rs"]
|
||||
#[path = "matchers_tests.rs"]
|
||||
mod tests;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use crate::completer::matchers::match_type_for_case_insensitive;
|
||||
|
||||
use super::{Match, MatchStrategy};
|
||||
use crate::completer::matchers::match_type_for_case_insensitive;
|
||||
|
||||
#[test]
|
||||
fn test_match_type_for_case_insensitive() {
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
use super::{Match, MatchStrategy};
|
||||
use crate::completer::matchers::match_type_for_case_insensitive;
|
||||
|
||||
#[test]
|
||||
fn test_match_type_for_case_insensitive() {
|
||||
assert_eq!(
|
||||
match_type_for_case_insensitive("git", "git"),
|
||||
Some(Match::Exact {
|
||||
is_case_sensitive: true
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
match_type_for_case_insensitive("gIt", "git"),
|
||||
Some(Match::Exact {
|
||||
is_case_sensitive: false
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
match_type_for_case_insensitive("abc", "abcdef"),
|
||||
Some(Match::Prefix {
|
||||
is_case_sensitive: true
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
match_type_for_case_insensitive("aBc", "abcdef"),
|
||||
Some(Match::Prefix {
|
||||
is_case_sensitive: false
|
||||
})
|
||||
);
|
||||
assert_eq!(match_type_for_case_insensitive("abc", "def"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_match_type_case_sensitive() {
|
||||
let matcher = MatchStrategy::CaseSensitive;
|
||||
|
||||
assert_eq!(matcher.get_match_type("git", "GIT"), None);
|
||||
assert_eq!(
|
||||
matcher.get_match_type("git", "git"),
|
||||
Some(Match::Exact {
|
||||
is_case_sensitive: true
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
matcher.get_match_type("AsDs", "AsDss"),
|
||||
Some(Match::Prefix {
|
||||
is_case_sensitive: true
|
||||
})
|
||||
);
|
||||
assert_eq!(matcher.get_match_type("Asds", "asds"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_match_type_case_insensitive() {
|
||||
let matcher = MatchStrategy::CaseInsensitive;
|
||||
|
||||
assert_eq!(
|
||||
matcher.get_match_type("git", "GIT"),
|
||||
Some(Match::Exact {
|
||||
is_case_sensitive: false
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
matcher.get_match_type("AsDs", "asdss"),
|
||||
Some(Match::Prefix {
|
||||
is_case_sensitive: false
|
||||
})
|
||||
);
|
||||
assert_eq!(matcher.get_match_type("Asd", "ads"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_match_type_fuzzy() {
|
||||
let matcher = MatchStrategy::Fuzzy;
|
||||
|
||||
assert_eq!(
|
||||
matcher.get_match_type("git", "GIT"),
|
||||
Some(Match::Exact {
|
||||
is_case_sensitive: false
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
matcher.get_match_type("AsDs", "asdss"),
|
||||
Some(Match::Prefix {
|
||||
is_case_sensitive: false
|
||||
})
|
||||
);
|
||||
assert!(matches!(
|
||||
matcher.get_match_type("abc", "aabac"),
|
||||
Some(Match::Fuzzy { .. })
|
||||
));
|
||||
assert_eq!(matcher.get_match_type("abc", "xyz"), None);
|
||||
}
|
||||
@@ -13,6 +13,8 @@ pub use context::{
|
||||
CommandExitStatus, CommandOutput, CompletionContext, GeneratorContext, PathCompletionContext,
|
||||
PathSeparators,
|
||||
};
|
||||
#[cfg(feature = "v2")]
|
||||
pub use context::{JsExecutionContext, JsExecutionError};
|
||||
pub use describe::{describe, describe_given_token, Description, TopLevelCommandCaseSensitivity};
|
||||
pub use engine::{EngineDirEntry, EngineFileType, LocationType};
|
||||
pub use matchers::{Match, MatchStrategy, MatchType};
|
||||
@@ -21,9 +23,6 @@ pub use suggest::{
|
||||
Suggestion, SuggestionResults, SuggestionType, SuggestionTypeName,
|
||||
};
|
||||
|
||||
#[cfg(feature = "v2")]
|
||||
pub use context::{JsExecutionContext, JsExecutionError};
|
||||
|
||||
fn get_path_separators(ctx: &dyn CompletionContext) -> PathSeparators {
|
||||
ctx.path_completion_context()
|
||||
.map(|ctx| ctx.path_separators())
|
||||
|
||||
@@ -276,5 +276,5 @@ fn expand_root_command_alias<'a>(
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "alias_test.rs"]
|
||||
#[path = "alias_tests.rs"]
|
||||
mod test;
|
||||
|
||||
@@ -18,7 +18,7 @@ pub fn test_expand_command_aliases() {
|
||||
.with_aliases(aliases);
|
||||
|
||||
// Simple case: there's a command we don't have an alias for
|
||||
let result = galaxyui::r#async::block_on(expand_command_aliases(
|
||||
let result = galaxyui_core::r#async::block_on(expand_command_aliases(
|
||||
"normalCommandWithoutAlias ",
|
||||
false,
|
||||
&ctx,
|
||||
@@ -31,7 +31,8 @@ pub fn test_expand_command_aliases() {
|
||||
assert!(result.signature_for_completions.is_none());
|
||||
|
||||
// We have a top-level "aliasForTest" which expands to "test".
|
||||
let result = galaxyui::r#async::block_on(expand_command_aliases("aliasForTest ", false, &ctx));
|
||||
let result =
|
||||
galaxyui_core::r#async::block_on(expand_command_aliases("aliasForTest ", false, &ctx));
|
||||
assert_eq!(result.expanded_command_line, "test ");
|
||||
assert_eq!(result.tokens_from_command, vec!["test"]);
|
||||
#[cfg(not(feature = "v2"))]
|
||||
@@ -48,7 +49,7 @@ pub fn test_expand_command_aliases() {
|
||||
{
|
||||
// The test signature has an alias function, which expands subcommand "twelve" to "one".
|
||||
let result =
|
||||
galaxyui::r#async::block_on(expand_command_aliases("test twelve ", false, &ctx));
|
||||
galaxyui_core::r#async::block_on(expand_command_aliases("test twelve ", false, &ctx));
|
||||
assert_eq!(result.expanded_command_line, "test one ");
|
||||
assert_eq!(result.tokens_from_command, vec!["test", "one"]);
|
||||
// Should be using the subcommand signature for completions
|
||||
@@ -62,7 +63,7 @@ pub fn test_expand_command_aliases() {
|
||||
);
|
||||
|
||||
// We have a top-level aliasForTest which expands to test, and then the test signature expands "twelve" to "one"
|
||||
let result = galaxyui::r#async::block_on(expand_command_aliases(
|
||||
let result = galaxyui_core::r#async::block_on(expand_command_aliases(
|
||||
"aliasForTest twelve ",
|
||||
false,
|
||||
&ctx,
|
||||
@@ -93,7 +94,7 @@ pub fn test_expand_command_aliases_env_vars() {
|
||||
.with_aliases(aliases);
|
||||
|
||||
// We have a top-level "aliasForTest" which expands to "test".
|
||||
let result = galaxyui::r#async::block_on(expand_command_aliases(
|
||||
let result = galaxyui_core::r#async::block_on(expand_command_aliases(
|
||||
"ENV1=VAL1 ENV2=VAL2 aliasForTest ",
|
||||
false,
|
||||
&ctx,
|
||||
@@ -122,7 +123,7 @@ pub fn test_expand_command_aliases_env_vars() {
|
||||
#[cfg(not(feature = "v2"))]
|
||||
{
|
||||
// The test signature has an alias function, which expands subcommand "twelve" to "one".
|
||||
let result = galaxyui::r#async::block_on(expand_command_aliases(
|
||||
let result = galaxyui_core::r#async::block_on(expand_command_aliases(
|
||||
"ENV1=VAL1 ENV2=VAL2 test twelve ",
|
||||
false,
|
||||
&ctx,
|
||||
@@ -152,7 +153,7 @@ pub fn test_expand_command_aliases_env_vars() {
|
||||
);
|
||||
|
||||
// We have a top-level aliasForTest which expands to test, and then the test signature expands "twelve" to "one"
|
||||
let result = galaxyui::r#async::block_on(expand_command_aliases(
|
||||
let result = galaxyui_core::r#async::block_on(expand_command_aliases(
|
||||
"ENV1=VAL1 ENV2=VAL2 aliasForTest twelve ",
|
||||
false,
|
||||
&ctx,
|
||||
@@ -195,13 +196,14 @@ pub fn test_expand_command_aliases_should_not_expand_if_no_space_after_alias() {
|
||||
.with_aliases(aliases);
|
||||
|
||||
// We have a top-level "aliasForTest" which expands to "test", but there's no trailing space so we shouldn't expand.
|
||||
let result = galaxyui::r#async::block_on(expand_command_aliases("aliasForTest", false, &ctx));
|
||||
let result =
|
||||
galaxyui_core::r#async::block_on(expand_command_aliases("aliasForTest", false, &ctx));
|
||||
assert_eq!(result.expanded_command_line, "aliasForTest");
|
||||
assert_eq!(result.tokens_from_command, vec!["aliasForTest"]);
|
||||
assert!(result.signature_for_completions.is_none());
|
||||
|
||||
// The test signature has an alias function which expands subcommand "twelve" to "one", but there's no trailing space so we shouldn't expand.
|
||||
let result = galaxyui::r#async::block_on(expand_command_aliases("test twelve", false, &ctx));
|
||||
let result = galaxyui_core::r#async::block_on(expand_command_aliases("test twelve", false, &ctx));
|
||||
assert_eq!(result.expanded_command_line, "test twelve");
|
||||
assert_eq!(result.tokens_from_command, vec!["test", "twelve"]);
|
||||
// "twelve" isn't a valid subcommand, so we should use the "test" signature.
|
||||
@@ -217,7 +219,7 @@ pub fn test_expand_command_aliases_should_not_expand_if_no_space_after_alias() {
|
||||
|
||||
// We have a top-level aliasForTest which expands to test. But the test signature does not expand "twelve" to "one" because there's no trailing space.
|
||||
let result =
|
||||
galaxyui::r#async::block_on(expand_command_aliases("aliasForTest twelve", false, &ctx));
|
||||
galaxyui_core::r#async::block_on(expand_command_aliases("aliasForTest twelve", false, &ctx));
|
||||
assert_eq!(result.expanded_command_line, "test twelve");
|
||||
assert_eq!(result.tokens_from_command, vec!["test", "twelve"]);
|
||||
// "twelve" isn't a valid subcommand, so we should use the "test" signature.
|
||||
@@ -244,13 +246,16 @@ pub fn test_expand_command_aliases_case_insensitive_for_powershell() {
|
||||
.with_aliases(aliases)
|
||||
.with_shell_family(ShellFamily::PowerShell);
|
||||
|
||||
let result = galaxyui::r#async::block_on(expand_command_aliases("ALIASFORTEST ", false, &ctx));
|
||||
let result =
|
||||
galaxyui_core::r#async::block_on(expand_command_aliases("ALIASFORTEST ", false, &ctx));
|
||||
assert_eq!(result.expanded_command_line, "test ");
|
||||
|
||||
let result = galaxyui::r#async::block_on(expand_command_aliases("aliasfortest ", false, &ctx));
|
||||
let result =
|
||||
galaxyui_core::r#async::block_on(expand_command_aliases("aliasfortest ", false, &ctx));
|
||||
assert_eq!(result.expanded_command_line, "test ");
|
||||
|
||||
let result = galaxyui::r#async::block_on(expand_command_aliases("ALIASFORTEST", false, &ctx));
|
||||
let result =
|
||||
galaxyui_core::r#async::block_on(expand_command_aliases("ALIASFORTEST", false, &ctx));
|
||||
assert_eq!(result.expanded_command_line, "ALIASFORTEST");
|
||||
}
|
||||
|
||||
@@ -266,10 +271,12 @@ pub fn test_expand_command_aliases_case_sensitive_for_posix() {
|
||||
.with_aliases(aliases)
|
||||
.with_shell_family(ShellFamily::Posix);
|
||||
|
||||
let result = galaxyui::r#async::block_on(expand_command_aliases("ALIASFORTEST ", false, &ctx));
|
||||
let result =
|
||||
galaxyui_core::r#async::block_on(expand_command_aliases("ALIASFORTEST ", false, &ctx));
|
||||
assert_eq!(result.expanded_command_line, "ALIASFORTEST ");
|
||||
|
||||
let result = galaxyui::r#async::block_on(expand_command_aliases("aliasForTest ", false, &ctx));
|
||||
let result =
|
||||
galaxyui_core::r#async::block_on(expand_command_aliases("aliasForTest ", false, &ctx));
|
||||
assert_eq!(result.expanded_command_line, "test ");
|
||||
}
|
||||
|
||||
@@ -285,7 +292,7 @@ pub fn test_expand_command_aliases_multiple_commands() {
|
||||
.with_aliases(aliases);
|
||||
|
||||
// We have a top-level "aliasForTest" which expands to "test".
|
||||
let result = galaxyui::r#async::block_on(expand_command_aliases(
|
||||
let result = galaxyui_core::r#async::block_on(expand_command_aliases(
|
||||
"kubectl get pod && ENV1=VAL1 ENV2=VAL2 aliasForTest ",
|
||||
false,
|
||||
&ctx,
|
||||
@@ -298,7 +305,7 @@ pub fn test_expand_command_aliases_multiple_commands() {
|
||||
#[cfg(not(feature = "v2"))]
|
||||
{
|
||||
// The test signature has an alias function, which expands subcommand "twelve" to "one".
|
||||
let result = galaxyui::r#async::block_on(expand_command_aliases(
|
||||
let result = galaxyui_core::r#async::block_on(expand_command_aliases(
|
||||
"kubectl get pod && ENV1=VAL1 ENV2=VAL2 test twelve ",
|
||||
false,
|
||||
&ctx,
|
||||
@@ -311,7 +318,7 @@ pub fn test_expand_command_aliases_multiple_commands() {
|
||||
// Multiple commands should all have their aliases expanded.
|
||||
// It is a known issue that only the last command is expanded currently.
|
||||
// TODO(INT-830): fix this case, it should expand to "ENV1=VAL1 ENV2=VAL2 test && ENV3=VAL3 ENV3=VAL3 test "
|
||||
let result = galaxyui::r#async::block_on(expand_command_aliases(
|
||||
let result = galaxyui_core::r#async::block_on(expand_command_aliases(
|
||||
"ENV1=VAL1 ENV2=VAL2 aliasForTest && ENV3=VAL3 ENV3=VAL3 aliasForTest ",
|
||||
false,
|
||||
&ctx,
|
||||
|
||||
@@ -0,0 +1,331 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use galaxy_util::path::ShellFamily;
|
||||
|
||||
use crate::completer::expand_command_aliases;
|
||||
use crate::completer::testing::{FakeCompletionContext, MockGeneratorContext};
|
||||
use crate::signatures::testing::{create_test_command_registry, test_signature};
|
||||
|
||||
#[test]
|
||||
pub fn test_expand_command_aliases() {
|
||||
let registry = create_test_command_registry([test_signature()]);
|
||||
|
||||
let generator_ctx = MockGeneratorContext::for_test_signature();
|
||||
let mut aliases = HashMap::new();
|
||||
aliases.insert("aliasForTest".into(), "test".to_owned());
|
||||
let ctx = FakeCompletionContext::new(registry)
|
||||
.with_generator_context(generator_ctx)
|
||||
.with_aliases(aliases);
|
||||
|
||||
// Simple case: there's a command we don't have an alias for
|
||||
let result = galaxyui_core::r#async::block_on(expand_command_aliases(
|
||||
"normalCommandWithoutAlias ",
|
||||
false,
|
||||
&ctx,
|
||||
));
|
||||
assert_eq!(result.expanded_command_line, "normalCommandWithoutAlias ");
|
||||
assert_eq!(
|
||||
result.tokens_from_command,
|
||||
vec!["normalCommandWithoutAlias"]
|
||||
);
|
||||
assert!(result.signature_for_completions.is_none());
|
||||
|
||||
// We have a top-level "aliasForTest" which expands to "test".
|
||||
let result =
|
||||
galaxyui_core::r#async::block_on(expand_command_aliases("aliasForTest ", false, &ctx));
|
||||
assert_eq!(result.expanded_command_line, "test ");
|
||||
assert_eq!(result.tokens_from_command, vec!["test"]);
|
||||
#[cfg(not(feature = "v2"))]
|
||||
assert_eq!(
|
||||
result
|
||||
.signature_for_completions
|
||||
.expect("should have signature for completions")
|
||||
.signature
|
||||
.name(),
|
||||
"test"
|
||||
);
|
||||
|
||||
#[cfg(not(feature = "v2"))]
|
||||
{
|
||||
// The test signature has an alias function, which expands subcommand "twelve" to "one".
|
||||
let result =
|
||||
galaxyui_core::r#async::block_on(expand_command_aliases("test twelve ", false, &ctx));
|
||||
assert_eq!(result.expanded_command_line, "test one ");
|
||||
assert_eq!(result.tokens_from_command, vec!["test", "one"]);
|
||||
// Should be using the subcommand signature for completions
|
||||
assert_eq!(
|
||||
result
|
||||
.signature_for_completions
|
||||
.expect("should have signature for completions")
|
||||
.signature
|
||||
.name(),
|
||||
"one"
|
||||
);
|
||||
|
||||
// We have a top-level aliasForTest which expands to test, and then the test signature expands "twelve" to "one"
|
||||
let result = galaxyui_core::r#async::block_on(expand_command_aliases(
|
||||
"aliasForTest twelve ",
|
||||
false,
|
||||
&ctx,
|
||||
));
|
||||
assert_eq!(result.expanded_command_line, "test one ");
|
||||
assert_eq!(result.tokens_from_command, vec!["test", "one"]);
|
||||
// Should be using the subcommand signature for completions
|
||||
assert_eq!(
|
||||
result
|
||||
.signature_for_completions
|
||||
.expect("should have signature for completions")
|
||||
.signature
|
||||
.name(),
|
||||
"one"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn test_expand_command_aliases_env_vars() {
|
||||
let registry = create_test_command_registry([test_signature()]);
|
||||
|
||||
let generator_ctx = MockGeneratorContext::for_test_signature();
|
||||
let mut aliases = HashMap::new();
|
||||
aliases.insert("aliasForTest".into(), "test".to_owned());
|
||||
let ctx = FakeCompletionContext::new(registry)
|
||||
.with_generator_context(generator_ctx)
|
||||
.with_aliases(aliases);
|
||||
|
||||
// We have a top-level "aliasForTest" which expands to "test".
|
||||
let result = galaxyui_core::r#async::block_on(expand_command_aliases(
|
||||
"ENV1=VAL1 ENV2=VAL2 aliasForTest ",
|
||||
false,
|
||||
&ctx,
|
||||
));
|
||||
assert_eq!(result.expanded_command_line, "ENV1=VAL1 ENV2=VAL2 test ");
|
||||
// Tokens should not include env vars
|
||||
assert_eq!(result.tokens_from_command, vec!["test"]);
|
||||
// Should have env vars in classified command.
|
||||
assert_eq!(
|
||||
result
|
||||
.classified_command
|
||||
.expect("should have classified command")
|
||||
.env_vars,
|
||||
vec!["ENV1=VAL1", "ENV2=VAL2"]
|
||||
);
|
||||
#[cfg(not(feature = "v2"))]
|
||||
assert_eq!(
|
||||
result
|
||||
.signature_for_completions
|
||||
.expect("should have signature for completions")
|
||||
.signature
|
||||
.name(),
|
||||
"test"
|
||||
);
|
||||
|
||||
#[cfg(not(feature = "v2"))]
|
||||
{
|
||||
// The test signature has an alias function, which expands subcommand "twelve" to "one".
|
||||
let result = galaxyui_core::r#async::block_on(expand_command_aliases(
|
||||
"ENV1=VAL1 ENV2=VAL2 test twelve ",
|
||||
false,
|
||||
&ctx,
|
||||
));
|
||||
assert_eq!(
|
||||
result.expanded_command_line,
|
||||
"ENV1=VAL1 ENV2=VAL2 test one "
|
||||
);
|
||||
// Tokens should not include env vars
|
||||
assert_eq!(result.tokens_from_command, vec!["test", "one"]);
|
||||
// Should have env vars in classified command.
|
||||
assert_eq!(
|
||||
result
|
||||
.classified_command
|
||||
.expect("should have classified command")
|
||||
.env_vars,
|
||||
vec!["ENV1=VAL1", "ENV2=VAL2"]
|
||||
);
|
||||
// Should be using the subcommand signature for completions
|
||||
assert_eq!(
|
||||
result
|
||||
.signature_for_completions
|
||||
.expect("should have signature for completions")
|
||||
.signature
|
||||
.name(),
|
||||
"one"
|
||||
);
|
||||
|
||||
// We have a top-level aliasForTest which expands to test, and then the test signature expands "twelve" to "one"
|
||||
let result = galaxyui_core::r#async::block_on(expand_command_aliases(
|
||||
"ENV1=VAL1 ENV2=VAL2 aliasForTest twelve ",
|
||||
false,
|
||||
&ctx,
|
||||
));
|
||||
assert_eq!(
|
||||
result.expanded_command_line,
|
||||
"ENV1=VAL1 ENV2=VAL2 test one "
|
||||
);
|
||||
// Tokens should not include env vars
|
||||
assert_eq!(result.tokens_from_command, vec!["test", "one"]);
|
||||
// Should have env vars in classified command.
|
||||
assert_eq!(
|
||||
result
|
||||
.classified_command
|
||||
.expect("should have classified command")
|
||||
.env_vars,
|
||||
vec!["ENV1=VAL1", "ENV2=VAL2"]
|
||||
);
|
||||
// Should be using the subcommand signature for completions
|
||||
assert_eq!(
|
||||
result
|
||||
.signature_for_completions
|
||||
.expect("should have signature for completions")
|
||||
.signature
|
||||
.name(),
|
||||
"one"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn test_expand_command_aliases_should_not_expand_if_no_space_after_alias() {
|
||||
let registry = create_test_command_registry([test_signature()]);
|
||||
|
||||
let generator_ctx = MockGeneratorContext::for_test_signature();
|
||||
let mut aliases = HashMap::new();
|
||||
aliases.insert("aliasForTest".into(), "test".to_owned());
|
||||
let ctx = FakeCompletionContext::new(registry)
|
||||
.with_generator_context(generator_ctx)
|
||||
.with_aliases(aliases);
|
||||
|
||||
// We have a top-level "aliasForTest" which expands to "test", but there's no trailing space so we shouldn't expand.
|
||||
let result =
|
||||
galaxyui_core::r#async::block_on(expand_command_aliases("aliasForTest", false, &ctx));
|
||||
assert_eq!(result.expanded_command_line, "aliasForTest");
|
||||
assert_eq!(result.tokens_from_command, vec!["aliasForTest"]);
|
||||
assert!(result.signature_for_completions.is_none());
|
||||
|
||||
// The test signature has an alias function which expands subcommand "twelve" to "one", but there's no trailing space so we shouldn't expand.
|
||||
let result = galaxyui_core::r#async::block_on(expand_command_aliases("test twelve", false, &ctx));
|
||||
assert_eq!(result.expanded_command_line, "test twelve");
|
||||
assert_eq!(result.tokens_from_command, vec!["test", "twelve"]);
|
||||
// "twelve" isn't a valid subcommand, so we should use the "test" signature.
|
||||
#[cfg(not(feature = "v2"))]
|
||||
assert_eq!(
|
||||
result
|
||||
.signature_for_completions
|
||||
.expect("should have signature for completions")
|
||||
.signature
|
||||
.name(),
|
||||
"test"
|
||||
);
|
||||
|
||||
// We have a top-level aliasForTest which expands to test. But the test signature does not expand "twelve" to "one" because there's no trailing space.
|
||||
let result =
|
||||
galaxyui_core::r#async::block_on(expand_command_aliases("aliasForTest twelve", false, &ctx));
|
||||
assert_eq!(result.expanded_command_line, "test twelve");
|
||||
assert_eq!(result.tokens_from_command, vec!["test", "twelve"]);
|
||||
// "twelve" isn't a valid subcommand, so we should use the "test" signature.
|
||||
#[cfg(not(feature = "v2"))]
|
||||
assert_eq!(
|
||||
result
|
||||
.signature_for_completions
|
||||
.expect("should have signature for completions")
|
||||
.signature
|
||||
.name(),
|
||||
"test"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn test_expand_command_aliases_case_insensitive_for_powershell() {
|
||||
let registry = create_test_command_registry([test_signature()]);
|
||||
|
||||
let generator_ctx = MockGeneratorContext::for_test_signature();
|
||||
let mut aliases = HashMap::new();
|
||||
aliases.insert("aliasForTest".into(), "test".to_owned());
|
||||
let ctx = FakeCompletionContext::new(registry)
|
||||
.with_generator_context(generator_ctx)
|
||||
.with_aliases(aliases)
|
||||
.with_shell_family(ShellFamily::PowerShell);
|
||||
|
||||
let result =
|
||||
galaxyui_core::r#async::block_on(expand_command_aliases("ALIASFORTEST ", false, &ctx));
|
||||
assert_eq!(result.expanded_command_line, "test ");
|
||||
|
||||
let result =
|
||||
galaxyui_core::r#async::block_on(expand_command_aliases("aliasfortest ", false, &ctx));
|
||||
assert_eq!(result.expanded_command_line, "test ");
|
||||
|
||||
let result =
|
||||
galaxyui_core::r#async::block_on(expand_command_aliases("ALIASFORTEST", false, &ctx));
|
||||
assert_eq!(result.expanded_command_line, "ALIASFORTEST");
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn test_expand_command_aliases_case_sensitive_for_posix() {
|
||||
let registry = create_test_command_registry([test_signature()]);
|
||||
|
||||
let generator_ctx = MockGeneratorContext::for_test_signature();
|
||||
let mut aliases = HashMap::new();
|
||||
aliases.insert("aliasForTest".into(), "test".to_owned());
|
||||
let ctx = FakeCompletionContext::new(registry)
|
||||
.with_generator_context(generator_ctx)
|
||||
.with_aliases(aliases)
|
||||
.with_shell_family(ShellFamily::Posix);
|
||||
|
||||
let result =
|
||||
galaxyui_core::r#async::block_on(expand_command_aliases("ALIASFORTEST ", false, &ctx));
|
||||
assert_eq!(result.expanded_command_line, "ALIASFORTEST ");
|
||||
|
||||
let result =
|
||||
galaxyui_core::r#async::block_on(expand_command_aliases("aliasForTest ", false, &ctx));
|
||||
assert_eq!(result.expanded_command_line, "test ");
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn test_expand_command_aliases_multiple_commands() {
|
||||
let registry = create_test_command_registry([test_signature()]);
|
||||
|
||||
let generator_ctx = MockGeneratorContext::for_test_signature();
|
||||
let mut aliases = HashMap::new();
|
||||
aliases.insert("aliasForTest".into(), "test".to_owned());
|
||||
let ctx = FakeCompletionContext::new(registry)
|
||||
.with_generator_context(generator_ctx)
|
||||
.with_aliases(aliases);
|
||||
|
||||
// We have a top-level "aliasForTest" which expands to "test".
|
||||
let result = galaxyui_core::r#async::block_on(expand_command_aliases(
|
||||
"kubectl get pod && ENV1=VAL1 ENV2=VAL2 aliasForTest ",
|
||||
false,
|
||||
&ctx,
|
||||
));
|
||||
assert_eq!(
|
||||
result.expanded_command_line,
|
||||
"kubectl get pod && ENV1=VAL1 ENV2=VAL2 test "
|
||||
);
|
||||
|
||||
#[cfg(not(feature = "v2"))]
|
||||
{
|
||||
// The test signature has an alias function, which expands subcommand "twelve" to "one".
|
||||
let result = galaxyui_core::r#async::block_on(expand_command_aliases(
|
||||
"kubectl get pod && ENV1=VAL1 ENV2=VAL2 test twelve ",
|
||||
false,
|
||||
&ctx,
|
||||
));
|
||||
assert_eq!(
|
||||
result.expanded_command_line,
|
||||
"kubectl get pod && ENV1=VAL1 ENV2=VAL2 test one "
|
||||
);
|
||||
|
||||
// Multiple commands should all have their aliases expanded.
|
||||
// It is a known issue that only the last command is expanded currently.
|
||||
// TODO(INT-830): fix this case, it should expand to "ENV1=VAL1 ENV2=VAL2 test && ENV3=VAL3 ENV3=VAL3 test "
|
||||
let result = galaxyui_core::r#async::block_on(expand_command_aliases(
|
||||
"ENV1=VAL1 ENV2=VAL2 aliasForTest && ENV3=VAL3 ENV3=VAL3 aliasForTest ",
|
||||
false,
|
||||
&ctx,
|
||||
));
|
||||
assert_eq!(
|
||||
result.expanded_command_line,
|
||||
"ENV1=VAL1 ENV2=VAL2 aliasForTest && ENV3=VAL3 ENV3=VAL3 test "
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,9 @@
|
||||
//! legacy command signature struct (`crate::signatures::CommandSignature`).
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::completer::{
|
||||
engine::{self, CompletionLocation},
|
||||
suggest::SuggestionTypeName,
|
||||
CompleterOptions, CompletionContext, LocationType, MatchedSuggestion,
|
||||
};
|
||||
use crate::completer::engine::{self, CompletionLocation};
|
||||
use crate::completer::suggest::SuggestionTypeName;
|
||||
use crate::completer::{CompleterOptions, CompletionContext, LocationType, MatchedSuggestion};
|
||||
use crate::parsers::{ClassifiedCommand, SignatureAtTokenIndex};
|
||||
|
||||
/// Returns a map of `SuggestionType` to vectors of `MatchedSuggestion`s for `line`.
|
||||
|
||||
@@ -3,33 +3,30 @@ pub mod alias;
|
||||
#[cfg_attr(not(feature = "v2"), path = "legacy.rs")]
|
||||
mod imp;
|
||||
mod priority;
|
||||
use alias::{expand_command_aliases, AliasExpansionResult};
|
||||
pub use priority::Priority;
|
||||
|
||||
use galaxy_core::ui::theme::AnsiColorIdentifier;
|
||||
use imp::*;
|
||||
|
||||
use std::cmp::Ordering;
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::{self, Display, Formatter};
|
||||
use std::hash::{Hash, Hasher};
|
||||
|
||||
use alias::{expand_command_aliases, AliasExpansionResult};
|
||||
use async_recursion::async_recursion;
|
||||
use imp::*;
|
||||
use itertools::Itertools;
|
||||
pub use priority::Priority;
|
||||
use smol_str::SmolStr;
|
||||
use warp_command_signatures::IconType;
|
||||
use galaxy_core::ui::theme::AnsiColorIdentifier;
|
||||
|
||||
use crate::parsers::simple::parse_for_completions;
|
||||
use crate::{completer::describe::OptionCaseSensitivity, parsers::classify_command};
|
||||
use crate::{completer::TopLevelCommandCaseSensitivity, meta::Span};
|
||||
|
||||
use super::coalesce::coalesce_completion_results;
|
||||
use super::context::CompletionContext;
|
||||
use super::engine::{self, completion_location};
|
||||
use super::{
|
||||
coalesce::coalesce_completion_results,
|
||||
context::CompletionContext,
|
||||
matchers::{Match, MatchStrategy, MatchType},
|
||||
EngineFileType,
|
||||
};
|
||||
use super::matchers::{Match, MatchStrategy, MatchType};
|
||||
use super::EngineFileType;
|
||||
use crate::completer::describe::OptionCaseSensitivity;
|
||||
use crate::completer::TopLevelCommandCaseSensitivity;
|
||||
use crate::meta::Span;
|
||||
use crate::parsers::classify_command;
|
||||
use crate::parsers::simple::parse_for_completions;
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct Suggestion {
|
||||
|
||||
@@ -95,5 +95,5 @@ impl From<Priority> for warp_command_signatures::Priority {
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "priority_test.rs"]
|
||||
#[path = "priority_tests.rs"]
|
||||
mod tests;
|
||||
|
||||
@@ -82,7 +82,6 @@ fn test_new_to_old_priority() {
|
||||
/// `warp_command_signatures` to the new Priority.
|
||||
#[test]
|
||||
fn test_old_to_new_priority() {
|
||||
use warp_command_signatures::{Importance, Order, Priority as OldPriority};
|
||||
|
||||
assert_eq!(
|
||||
Priority::from(OldPriority::Global(Importance::Less(Order(1)))),
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
use super::{Priority, MAX_PRIORITY, MIN_PRIORITY};
|
||||
|
||||
#[test]
|
||||
fn test_priority_normalization() {
|
||||
let too_small = Priority::new(-201);
|
||||
assert_eq!(Priority::min(), too_small);
|
||||
|
||||
let too_large = Priority::new(201);
|
||||
assert_eq!(Priority::max(), too_large);
|
||||
|
||||
let fourty_two = Priority::new(42);
|
||||
assert_eq!(42, fourty_two.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_priority_comparison() {
|
||||
let super_important = Priority::new(100);
|
||||
let important = Priority::new(20);
|
||||
let not_important = Priority::new(-60);
|
||||
let default = Priority::default();
|
||||
|
||||
assert!(super_important == super_important);
|
||||
assert!(super_important > important);
|
||||
assert!(super_important > default);
|
||||
assert!(super_important > not_important);
|
||||
|
||||
assert!(important < super_important);
|
||||
assert!(important == important);
|
||||
assert!(important > default);
|
||||
assert!(important > not_important);
|
||||
|
||||
assert!(not_important < super_important);
|
||||
assert!(not_important < important);
|
||||
assert!(not_important < default);
|
||||
assert!(not_important == not_important);
|
||||
|
||||
assert!(default == default);
|
||||
}
|
||||
|
||||
/// Test that we can correctly convert from the new Priority to the original as defined in
|
||||
/// `warp_command_signatures`.
|
||||
#[test]
|
||||
fn test_new_to_old_priority() {
|
||||
use warp_command_signatures::{Importance, Order, Priority as OldPriority};
|
||||
assert_eq!(
|
||||
OldPriority::from(Priority::new(MIN_PRIORITY)),
|
||||
OldPriority::Global(Importance::Less(Order(1))),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
OldPriority::from(Priority::new(-51)),
|
||||
OldPriority::Global(Importance::Less(Order(50))),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
OldPriority::from(Priority::new(-1)),
|
||||
OldPriority::Global(Importance::Less(Order(100)))
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
OldPriority::from(Priority::default()),
|
||||
OldPriority::default()
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
OldPriority::from(Priority::new(1)),
|
||||
OldPriority::Global(Importance::More(Order(1))),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
OldPriority::from(Priority::new(50)),
|
||||
OldPriority::Global(Importance::More(Order(50))),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
OldPriority::from(Priority::new(MAX_PRIORITY)),
|
||||
OldPriority::Global(Importance::More(Order(100))),
|
||||
);
|
||||
}
|
||||
|
||||
/// Test that we can correctly convert from the old Priority as definined in
|
||||
/// `warp_command_signatures` to the new Priority.
|
||||
#[test]
|
||||
fn test_old_to_new_priority() {
|
||||
|
||||
assert_eq!(
|
||||
Priority::from(OldPriority::Global(Importance::Less(Order(1)))),
|
||||
Priority::new(MIN_PRIORITY)
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
Priority::from(OldPriority::Global(Importance::Less(Order(50)))),
|
||||
Priority::new(-51)
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
Priority::from(OldPriority::Global(Importance::Less(Order(100)))),
|
||||
Priority::new(-1)
|
||||
);
|
||||
|
||||
assert_eq!(Priority::from(OldPriority::Default), Priority::default());
|
||||
|
||||
assert_eq!(
|
||||
Priority::from(OldPriority::Global(Importance::More(Order(1)))),
|
||||
Priority::new(1)
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
Priority::from(OldPriority::Global(Importance::More(Order(50)))),
|
||||
Priority::new(50)
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
Priority::from(OldPriority::Global(Importance::More(Order(100)))),
|
||||
Priority::new(MAX_PRIORITY)
|
||||
);
|
||||
}
|
||||
@@ -3,6 +3,9 @@ use std::iter::FromIterator;
|
||||
|
||||
use typed_path::TypedPathBuf;
|
||||
|
||||
use super::{
|
||||
suggestions, CompleterOptions, CompletionsFallbackStrategy, SuggestionResults, SuggestionType,
|
||||
};
|
||||
use crate::completer::context::CompletionContext;
|
||||
use crate::completer::engine::EngineDirEntry;
|
||||
use crate::completer::matchers::MatchStrategy;
|
||||
@@ -16,9 +19,6 @@ use crate::signatures::testing::{
|
||||
};
|
||||
use crate::signatures::CommandRegistry;
|
||||
|
||||
use super::CompleterOptions;
|
||||
use super::{suggestions, CompletionsFallbackStrategy, SuggestionResults, SuggestionType};
|
||||
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(not(feature = "v2"))] {
|
||||
use std::collections::HashSet;
|
||||
@@ -51,7 +51,7 @@ fn suggestions_for_test<T: CompletionContext>(
|
||||
options: CompleterOptions,
|
||||
ctx: &T,
|
||||
) -> Option<SuggestionResults> {
|
||||
galaxyui::r#async::block_on(suggestions(line, pos, None, options, ctx))
|
||||
galaxyui_core::r#async::block_on(suggestions(line, pos, None, options, ctx))
|
||||
}
|
||||
|
||||
/// Runs the completer at the end of the given line and returns the associated
|
||||
@@ -354,7 +354,7 @@ pub fn test_completes_dotfiles() {
|
||||
vec!["./", "../", ".bar/"],
|
||||
);
|
||||
|
||||
// Dotfiles should not be included if the the path does not start with a dot.
|
||||
// Dotfiles should not be included if the path does not start with a dot.
|
||||
assert_eq!(
|
||||
complete_at_end_of_line("cat ", &ctx),
|
||||
vec!["foo/", "foobar"],
|
||||
@@ -1682,7 +1682,7 @@ fn test_hidden_suggestion_only_appears_on_exact_match() {
|
||||
.with_entries_in_pwd([EngineDirEntry::test_dir("app")]);
|
||||
let ctx = FakeCompletionContext::new(registry).with_path_completion_context(path_ctx);
|
||||
|
||||
// Need to use filter_by_query here to incorporate the the exact match logic.
|
||||
// Need to use filter_by_query here to incorporate the exact match logic.
|
||||
let suggestion_results =
|
||||
complete_at_end_of_line_with_query("cd ", "", MatchStrategy::CaseInsensitive, &ctx);
|
||||
assert_eq!(suggestion_results, vec!["app/"]);
|
||||
@@ -2089,7 +2089,7 @@ fn test_exact_match_completions() {
|
||||
);
|
||||
|
||||
// Exact match suggestion should be the first suggestion even if others have more priority.
|
||||
// Need to use filter_by_query here to incorporate the the exact match logic.
|
||||
// Need to use filter_by_query here to incorporate the exact match logic.
|
||||
let suggestions = complete_at_end_of_line_with_query(
|
||||
"test six six-arg",
|
||||
"six-arg",
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
//! JS-compatible command signatures struct (crate::signatures::CommandSignature).
|
||||
use std::collections::HashMap;
|
||||
|
||||
use super::{CompleterOptions, CompletionContext, MatchedSuggestion, SuggestionTypeName};
|
||||
use crate::completer::engine::{self, CompletionLocation};
|
||||
use crate::completer::LocationType;
|
||||
use crate::{parsers::ClassifiedCommand, signatures::Command};
|
||||
|
||||
use super::{CompleterOptions, CompletionContext, MatchedSuggestion, SuggestionTypeName};
|
||||
use crate::parsers::ClassifiedCommand;
|
||||
use crate::signatures::Command;
|
||||
|
||||
/// Returns a map of `SuggestionType` to vectors of `MatchedSuggestion`s for `input`.
|
||||
///
|
||||
|
||||
@@ -2,12 +2,10 @@
|
||||
#[cfg(feature = "v2")]
|
||||
mod v2;
|
||||
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
ops::Deref,
|
||||
path::PathBuf,
|
||||
sync::Arc,
|
||||
};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::ops::Deref;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use galaxy_core::command::ExitCode;
|
||||
@@ -16,18 +14,15 @@ use smol_str::SmolStr;
|
||||
use typed_path::{TypedPath, TypedPathBuf};
|
||||
use warp_command_signatures::IconType;
|
||||
|
||||
use crate::{
|
||||
completer::{
|
||||
CommandOutput, CompletionContext, Description, EngineDirEntry, EngineFileType,
|
||||
GeneratorContext, PathCompletionContext, Suggestion, TopLevelCommandCaseSensitivity,
|
||||
},
|
||||
signatures::{
|
||||
testing::{TEST_ALIAS_COMMAND, TEST_GENERATOR_1_COMMAND, TEST_GENERATOR_2_COMMAND},
|
||||
CommandRegistry,
|
||||
},
|
||||
};
|
||||
|
||||
use super::{CommandExitStatus, MatchedSuggestion, PathSeparators};
|
||||
use crate::completer::{
|
||||
CommandOutput, CompletionContext, Description, EngineDirEntry, EngineFileType,
|
||||
GeneratorContext, PathCompletionContext, Suggestion, TopLevelCommandCaseSensitivity,
|
||||
};
|
||||
use crate::signatures::testing::{
|
||||
TEST_ALIAS_COMMAND, TEST_GENERATOR_1_COMMAND, TEST_GENERATOR_2_COMMAND,
|
||||
};
|
||||
use crate::signatures::CommandRegistry;
|
||||
|
||||
impl EngineDirEntry {
|
||||
pub fn test_file(file_name: &str) -> Self {
|
||||
@@ -141,6 +136,7 @@ impl GeneratorContext for MockGeneratorContext {
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MockPathCompletionContext {
|
||||
home_directory: Option<String>,
|
||||
cdpath: Option<String>,
|
||||
pwd: TypedPathBuf,
|
||||
directory_to_entries: HashMap<PathBuf, Vec<EngineDirEntry>>,
|
||||
}
|
||||
@@ -149,6 +145,7 @@ impl MockPathCompletionContext {
|
||||
pub fn new(pwd: TypedPathBuf) -> Self {
|
||||
Self {
|
||||
home_directory: TEST_SESSION_HOME_DIR.clone(),
|
||||
cdpath: None,
|
||||
pwd,
|
||||
directory_to_entries: HashMap::new(),
|
||||
}
|
||||
@@ -159,6 +156,11 @@ impl MockPathCompletionContext {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_cdpath(mut self, cdpath: String) -> Self {
|
||||
self.cdpath = Some(cdpath);
|
||||
self
|
||||
}
|
||||
|
||||
/// The given entries are mocked as children of the context's `pwd`, such that `entries`
|
||||
/// is returned if the completions engine calls
|
||||
/// `path_ctx.list_directory_entries(path_ctx.pwd())`.
|
||||
@@ -233,6 +235,10 @@ impl PathCompletionContext for MockPathCompletionContext {
|
||||
self.home_directory.as_deref()
|
||||
}
|
||||
|
||||
fn cdpath(&self) -> Option<&str> {
|
||||
self.cdpath.as_deref()
|
||||
}
|
||||
|
||||
fn pwd(&self) -> TypedPath<'_> {
|
||||
self.pwd.to_path()
|
||||
}
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
use async_trait::async_trait;
|
||||
use galaxy_js::{JsFunctionId, SerializedJsValue};
|
||||
|
||||
use crate::{
|
||||
completer::context::{JsExecutionContext, JsExecutionError},
|
||||
signatures::{
|
||||
testing::{TEST_GENERATOR_1_JS_FUNCTION, TEST_GENERATOR_2_JS_FUNCTION},
|
||||
GeneratorResults, Suggestion,
|
||||
},
|
||||
};
|
||||
use crate::completer::context::{JsExecutionContext, JsExecutionError};
|
||||
use crate::signatures::testing::{TEST_GENERATOR_1_JS_FUNCTION, TEST_GENERATOR_2_JS_FUNCTION};
|
||||
use crate::signatures::{GeneratorResults, Suggestion};
|
||||
|
||||
pub struct FakeJsExecutionContext {}
|
||||
|
||||
|
||||
@@ -217,5 +217,5 @@ where
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "meta_test.rs"]
|
||||
#[path = "meta_tests.rs"]
|
||||
mod tests;
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
use super::*;
|
||||
|
||||
/*
|
||||
0 1 2 3
|
||||
w a r p
|
||||
-------
|
||||
0 4 << the span for the string "warp" is (0, 4)
|
||||
|
||||
Spanned {
|
||||
item: String::new("warp"), << warp string
|
||||
span: Span::new(0, 4) << span
|
||||
}
|
||||
|
||||
or >> String::new("warp").spanned(Span::new(0, 4)) */
|
||||
fn warp() -> Spanned<String> {
|
||||
String::from("warp").spanned(Span::new(0, 4))
|
||||
}
|
||||
|
||||
fn empty() -> Spanned<String> {
|
||||
String::new().spanned_unknown()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn knows_distances() {
|
||||
assert!(warp().span.distance() == 4);
|
||||
assert!(empty().span.distance() == 0);
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
#![allow(dead_code)]
|
||||
use crate::meta::{Span, Spanned, SpannedItem};
|
||||
use getset::Getters;
|
||||
|
||||
use crate::meta::{Span, Spanned, SpannedItem};
|
||||
|
||||
#[derive(Getters, Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
|
||||
pub struct ParseError {
|
||||
#[get = "pub"]
|
||||
|
||||
@@ -3,20 +3,16 @@
|
||||
use itertools::Itertools;
|
||||
use warp_command_signatures::{DynamicCompletionData, IsArgumentOptional, Opt, Signature};
|
||||
|
||||
use crate::{
|
||||
completer::TopLevelCommandCaseSensitivity,
|
||||
meta::{HasSpan, Span, Spanned, SpannedItem},
|
||||
parsers::{
|
||||
hir::Flags, parse_arg, parse_dollar_expr, ArgumentError, FlagArgumentsCardinality,
|
||||
FlagSignature, ParsedExpression, ParsedToken,
|
||||
},
|
||||
signatures::CommandRegistry,
|
||||
};
|
||||
|
||||
use super::{
|
||||
hir::{Command, Expression, ShellCommand},
|
||||
parse_unclassified_command, LiteCommand, ParseError,
|
||||
use super::hir::{Command, Expression, ShellCommand};
|
||||
use super::{parse_unclassified_command, LiteCommand, ParseError};
|
||||
use crate::completer::TopLevelCommandCaseSensitivity;
|
||||
use crate::meta::{HasSpan, Span, Spanned, SpannedItem};
|
||||
use crate::parsers::hir::Flags;
|
||||
use crate::parsers::{
|
||||
parse_arg, parse_dollar_expr, ArgumentError, FlagArgumentsCardinality, FlagSignature,
|
||||
ParsedExpression, ParsedToken,
|
||||
};
|
||||
use crate::signatures::CommandRegistry;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
/// A `Signature` (and its corresponding generator) contained at a given index.
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
#[cfg_attr(feature = "v2", path = "v2.rs")]
|
||||
#[cfg_attr(not(feature = "v2"), path = "legacy.rs")]
|
||||
mod imp;
|
||||
use imp::*;
|
||||
|
||||
#[cfg(not(feature = "v2"))]
|
||||
pub use imp::SignatureAtTokenIndex;
|
||||
use imp::*;
|
||||
|
||||
mod errors;
|
||||
pub use errors::{ArgumentError, ParseError, ParseErrorReason};
|
||||
@@ -12,18 +11,15 @@ pub mod hir;
|
||||
pub mod simple;
|
||||
|
||||
use derive_new::new;
|
||||
use hir::{ArgType, Command, Expression, ExternalCommand};
|
||||
use itertools::Itertools;
|
||||
use lazy_static::lazy_static;
|
||||
use regex::Regex;
|
||||
use warp_command_signatures::Argument;
|
||||
|
||||
use crate::completer::TopLevelCommandCaseSensitivity;
|
||||
use crate::meta::{HasSpan, Span, Spanned, SpannedItem};
|
||||
use crate::signatures::CommandRegistry;
|
||||
use crate::{
|
||||
completer::TopLevelCommandCaseSensitivity,
|
||||
meta::{HasSpan, Span, Spanned, SpannedItem},
|
||||
};
|
||||
|
||||
use hir::{ArgType, Command, Expression, ExternalCommand};
|
||||
|
||||
lazy_static! {
|
||||
// Regex to test for a valid environment variable name, Environment variable names used by the
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use std::fmt;
|
||||
|
||||
use super::{Command, Part};
|
||||
use crate::meta::{Span, Spanned};
|
||||
use crate::parsers::{LiteCommand, LiteGroup, LitePipeline, LiteRootNode};
|
||||
use std::fmt;
|
||||
|
||||
impl From<Spanned<Command>> for LiteCommand {
|
||||
fn from(command: Spanned<Command>) -> Self {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use super::{token::Token, EscapeChar};
|
||||
use super::token::Token;
|
||||
use super::EscapeChar;
|
||||
use crate::meta::{Span, Spanned, SpannedItem};
|
||||
|
||||
/// Iterator for converting a string into a series of Tokens
|
||||
@@ -214,5 +215,5 @@ impl<'a> Iterator for Lexer<'a> {
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "lexer_test.rs"]
|
||||
#[path = "lexer_tests.rs"]
|
||||
mod tests;
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_lexer() {
|
||||
let source = r#"ls | rm -rf || touch 'hello.txt' &
|
||||
cat "Hello $(ls -la)" && echo `ps \`; {echo Goodbye😀}"#;
|
||||
let tokens: Vec<_> = Lexer::new(source, EscapeChar::Backslash, false)
|
||||
.map(|t| t.item)
|
||||
.collect();
|
||||
|
||||
assert_eq!(
|
||||
tokens,
|
||||
[
|
||||
Token::Literal("ls"),
|
||||
Token::Whitespace(" "),
|
||||
Token::Pipe,
|
||||
Token::Whitespace(" "),
|
||||
Token::Literal("rm"),
|
||||
Token::Whitespace(" "),
|
||||
Token::Literal("-rf"),
|
||||
Token::Whitespace(" "),
|
||||
Token::LogicalOr,
|
||||
Token::Whitespace(" "),
|
||||
Token::Literal("touch"),
|
||||
Token::Whitespace(" "),
|
||||
Token::SingleQuote,
|
||||
Token::Literal("hello.txt"),
|
||||
Token::SingleQuote,
|
||||
Token::Whitespace(" "),
|
||||
Token::Ampersand,
|
||||
Token::Newline,
|
||||
Token::Literal("cat"),
|
||||
Token::Whitespace(" "),
|
||||
Token::DoubleQuote,
|
||||
Token::Literal("Hello"),
|
||||
Token::Whitespace(" "),
|
||||
Token::Dollar,
|
||||
Token::OpenParen,
|
||||
Token::Literal("ls"),
|
||||
Token::Whitespace(" "),
|
||||
Token::Literal("-la"),
|
||||
Token::CloseParen,
|
||||
Token::DoubleQuote,
|
||||
Token::Whitespace(" "),
|
||||
Token::LogicalAnd,
|
||||
Token::Whitespace(" "),
|
||||
Token::Literal("echo"),
|
||||
Token::Whitespace(" "),
|
||||
Token::Backtick,
|
||||
Token::Literal("ps"),
|
||||
Token::Whitespace(" "),
|
||||
Token::EscapeChar("\\"),
|
||||
Token::Backtick,
|
||||
Token::Semicolon,
|
||||
Token::Whitespace(" "),
|
||||
Token::OpenCurly,
|
||||
Token::Literal("echo"),
|
||||
Token::Whitespace(" "),
|
||||
Token::Literal("Goodbye😀"),
|
||||
Token::CloseCurly,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_spans() {
|
||||
let source = "ls -la && echo Hello' World'$(cat 😀.txt";
|
||||
let spans: Vec<_> = Lexer::new(source, EscapeChar::Backslash, false)
|
||||
.map(|t| t.span)
|
||||
.collect();
|
||||
|
||||
assert_eq!(
|
||||
spans,
|
||||
[
|
||||
Span::new(0, 2), // ls
|
||||
Span::new(2, 3), // space
|
||||
Span::new(3, 6), // -la
|
||||
Span::new(6, 7), // space
|
||||
Span::new(7, 9), // &&
|
||||
Span::new(9, 10), // space
|
||||
Span::new(10, 14), // echo
|
||||
Span::new(14, 15), // space
|
||||
Span::new(15, 20), // Hello
|
||||
Span::new(20, 21), // '
|
||||
Span::new(21, 22), // space
|
||||
Span::new(22, 27), // World
|
||||
Span::new(27, 28), // '
|
||||
Span::new(28, 29), // $
|
||||
Span::new(29, 30), // (
|
||||
Span::new(30, 33), // cat
|
||||
Span::new(33, 35), // double space
|
||||
Span::new(35, 43), // 😀.txt (😀 is 4 bytes long)
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_escaped_tokens() {
|
||||
let source = r"\\\||\&&\\&&||";
|
||||
let tokens: Vec<_> = Lexer::new(source, EscapeChar::Backslash, false)
|
||||
.map(|t| t.item)
|
||||
.collect();
|
||||
|
||||
assert_eq!(
|
||||
tokens,
|
||||
[
|
||||
Token::EscapeChar("\\"),
|
||||
Token::EscapeChar("\\"),
|
||||
Token::EscapeChar("\\"),
|
||||
Token::Pipe,
|
||||
Token::Pipe,
|
||||
Token::EscapeChar("\\"),
|
||||
Token::Ampersand,
|
||||
Token::Ampersand,
|
||||
Token::EscapeChar("\\"),
|
||||
Token::EscapeChar("\\"),
|
||||
Token::LogicalAnd,
|
||||
Token::LogicalOr,
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_escaped_token_spans() {
|
||||
let source = r"\\\||\&&\\&&||";
|
||||
let spans: Vec<_> = Lexer::new(source, EscapeChar::Backslash, false)
|
||||
.map(|t| t.span)
|
||||
.collect();
|
||||
|
||||
assert_eq!(
|
||||
spans,
|
||||
[
|
||||
Span::new(0, 1), // \
|
||||
Span::new(1, 2), // \
|
||||
Span::new(2, 3), // \
|
||||
Span::new(3, 4), // |
|
||||
Span::new(4, 5), // |
|
||||
Span::new(5, 6), // \
|
||||
Span::new(6, 7), // &
|
||||
Span::new(7, 8), // &
|
||||
Span::new(8, 9), // \
|
||||
Span::new(9, 10), // \
|
||||
Span::new(10, 12), // &&
|
||||
Span::new(12, 14), // ||
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multiple_whitespace() {
|
||||
let source = " \t |\t ";
|
||||
let tokens: Vec<_> = Lexer::new(source, EscapeChar::Backslash, false)
|
||||
.map(|t| t.item)
|
||||
.collect();
|
||||
|
||||
assert_eq!(
|
||||
tokens,
|
||||
[
|
||||
Token::Whitespace(" \t "),
|
||||
Token::Pipe,
|
||||
Token::Whitespace("\t "),
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_backtick_escape_char() {
|
||||
let source = r#"& "$HOME\Downloads\Warp` Setup.exe" /SP- /SILENT `t`"#;
|
||||
let tokens: Vec<_> = Lexer::new(source, EscapeChar::Backtick, false)
|
||||
.map(|t| (t.item, t.span))
|
||||
.collect();
|
||||
|
||||
assert_eq!(
|
||||
tokens,
|
||||
[
|
||||
(Token::Ampersand, Span::new(0, 1)),
|
||||
(Token::Whitespace(" "), Span::new(1, 2)),
|
||||
(Token::DoubleQuote, Span::new(2, 3)),
|
||||
(Token::Dollar, Span::new(3, 4)),
|
||||
(Token::Literal(r"HOME\Downloads\Warp"), Span::new(4, 23)),
|
||||
(Token::EscapeChar("`"), Span::new(23, 24)),
|
||||
(Token::Whitespace(" "), Span::new(24, 25)),
|
||||
(Token::Literal("Setup.exe"), Span::new(25, 34)),
|
||||
(Token::DoubleQuote, Span::new(34, 35)),
|
||||
(Token::Whitespace(" "), Span::new(35, 36)),
|
||||
(Token::Literal("/SP-"), Span::new(36, 40)),
|
||||
(Token::Whitespace(" "), Span::new(40, 41)),
|
||||
(Token::Literal("/SILENT"), Span::new(41, 48)),
|
||||
(Token::Whitespace(" "), Span::new(48, 49)),
|
||||
(Token::EscapeChar("`"), Span::new(49, 50)),
|
||||
(Token::Literal("t"), Span::new(50, 51)),
|
||||
(Token::EscapeChar("`"), Span::new(51, 52)),
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_single_quote_as_literals() {
|
||||
let source = r#"I'd like to edit app/src"#;
|
||||
let tokens: Vec<_> = Lexer::new(source, EscapeChar::Backslash, true)
|
||||
.map(|t| (t.item, t.span))
|
||||
.collect();
|
||||
|
||||
assert_eq!(
|
||||
tokens,
|
||||
[
|
||||
(Token::Literal("I'd"), Span::new(0, 3)),
|
||||
(Token::Whitespace(" "), Span::new(3, 4)),
|
||||
(Token::Literal("like"), Span::new(4, 8)),
|
||||
(Token::Whitespace(" "), Span::new(8, 9)),
|
||||
(Token::Literal("to"), Span::new(9, 11)),
|
||||
(Token::Whitespace(" "), Span::new(11, 12)),
|
||||
(Token::Literal("edit"), Span::new(12, 16)),
|
||||
(Token::Whitespace(" "), Span::new(16, 17)),
|
||||
(Token::Literal("app/src"), Span::new(17, 24)),
|
||||
]
|
||||
)
|
||||
}
|
||||
@@ -6,12 +6,12 @@ mod lexer;
|
||||
mod parser;
|
||||
mod token;
|
||||
|
||||
use crate::parsers::LiteCommand;
|
||||
use galaxy_util::path::EscapeChar;
|
||||
use lexer::Lexer;
|
||||
use parser::Parser;
|
||||
|
||||
use string_offset::ByteOffset;
|
||||
use galaxy_util::path::EscapeChar;
|
||||
|
||||
use crate::parsers::LiteCommand;
|
||||
|
||||
/// Parse the input and return the last unclosed command to complete on using the completions
|
||||
/// infrastructure.
|
||||
@@ -89,6 +89,21 @@ pub fn all_parsed_commands<S: AsRef<str>>(
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns the source command with leading env-var assignments removed.
|
||||
///
|
||||
/// For example, if the source is "PAGER=0 git log", this returns "git log".
|
||||
pub fn command_without_leading_env_vars<S: AsRef<str>>(
|
||||
source: S,
|
||||
escape_char: EscapeChar,
|
||||
) -> Option<String> {
|
||||
let source = source.as_ref();
|
||||
let parser = Parser::new(Lexer::new(source, escape_char, false));
|
||||
let mut command = parser.parse().commands.into_iter().next()?;
|
||||
command.item.remove_leading_env_vars();
|
||||
|
||||
command.item.source(source)
|
||||
}
|
||||
|
||||
/// Given a `command` string, returns:
|
||||
/// 1. the subcommands that make it up, including the recomposed commands at each level of nesting.
|
||||
/// For example, given "ls $(foo | echo)", this API returns ["foo", "echo", "foo | echo", "ls $(foo | echo)"]
|
||||
@@ -181,11 +196,7 @@ impl Command {
|
||||
}
|
||||
|
||||
pub fn decompose(self, src: &str) -> Vec<String> {
|
||||
let this_command = self
|
||||
.parts
|
||||
.first()
|
||||
.zip(self.parts.last())
|
||||
.map(|(first, last)| src[first.span.start()..last.span.end()].trim().to_string());
|
||||
let this_command = self.source(src);
|
||||
|
||||
let mut all_commands = vec![];
|
||||
let mut this_command_has_literal = false;
|
||||
@@ -228,6 +239,13 @@ impl Command {
|
||||
all_commands
|
||||
}
|
||||
|
||||
fn source(&self, src: &str) -> Option<String> {
|
||||
self.parts
|
||||
.first()
|
||||
.zip(self.parts.last())
|
||||
.map(|(first, last)| src[first.span.start()..last.span.end()].trim().to_string())
|
||||
}
|
||||
|
||||
/// Removes the leading env-var assignments (i.e. 'KEY=VALUE' literals) from the command.
|
||||
pub fn remove_leading_env_vars(&mut self) {
|
||||
while !self.parts.is_empty() {
|
||||
|
||||
@@ -378,7 +378,6 @@ fn is_valid_command_separator(token: &Token) -> bool {
|
||||
///
|
||||
/// This includes all of the separator tokens as well as the grouping tokens
|
||||
fn is_command_terminator(token: &Token) -> bool {
|
||||
use Token::*;
|
||||
|
||||
is_valid_command_separator(token)
|
||||
|| matches!(token, OpenParen | CloseParen | OpenCurly | CloseCurly)
|
||||
@@ -456,5 +455,5 @@ impl PartBuilder {
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "parser_test.rs"]
|
||||
#[path = "parser_tests.rs"]
|
||||
mod tests;
|
||||
|
||||
@@ -2,11 +2,12 @@ use std::collections::HashSet;
|
||||
|
||||
use galaxy_util::path::EscapeChar;
|
||||
|
||||
use crate::parsers::simple::{decompose_command, top_level_command};
|
||||
|
||||
use super::super::lexer::Lexer;
|
||||
use super::super::{Command, Part};
|
||||
use super::*;
|
||||
use crate::parsers::simple::{
|
||||
command_without_leading_env_vars, decompose_command, top_level_command,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn test_parse_open_subshell() {
|
||||
@@ -162,6 +163,26 @@ fn test_decompose_command() {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_command_without_leading_env_vars() {
|
||||
let test_data = vec![
|
||||
("X=1 rm -rf target", Some("rm -rf target")),
|
||||
(
|
||||
"X=1 Y=2 curl https://example.com",
|
||||
Some("curl https://example.com"),
|
||||
),
|
||||
("rm -rf target", Some("rm -rf target")),
|
||||
("X=1", None),
|
||||
];
|
||||
|
||||
for (input, expected_output) in test_data {
|
||||
assert_eq!(
|
||||
command_without_leading_env_vars(input, EscapeChar::Backslash),
|
||||
expected_output.map(ToString::to_string)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_contains_redirection() {
|
||||
let test_data = vec![
|
||||
|
||||
@@ -0,0 +1,223 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use galaxy_util::path::EscapeChar;
|
||||
|
||||
use super::super::lexer::Lexer;
|
||||
use super::super::{Command, Part};
|
||||
use super::*;
|
||||
use crate::parsers::simple::{
|
||||
command_without_leading_env_vars, decompose_command, top_level_command,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn test_parse_open_subshell() {
|
||||
let source = r#"cat "Hello $(ls -la"#;
|
||||
let command = Parser::new(Lexer::new(source, EscapeChar::Backslash, false)).parse_command();
|
||||
|
||||
assert_eq!(
|
||||
command,
|
||||
Command::new(vec![
|
||||
Part::Literal("cat".into()).spanned((0, 3)),
|
||||
Part::Concatenated(vec![
|
||||
Part::Literal("Hello ".into()).spanned((4, 11)),
|
||||
Part::OpenSubshell(vec![Command::new(vec![
|
||||
Part::Literal("ls".into()).spanned((13, 15)),
|
||||
Part::Literal("-la".into()).spanned((16, 19)),
|
||||
])
|
||||
.spanned((13, 19)),])
|
||||
.spanned((11, 19)),
|
||||
])
|
||||
.spanned((4, 19)),
|
||||
])
|
||||
.spanned((0, 19)),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_nested_command() {
|
||||
let source = r#"cat "Hello $(ls -la)""#;
|
||||
let command = Parser::new(Lexer::new(source, EscapeChar::Backslash, false)).parse_command();
|
||||
|
||||
assert_eq!(
|
||||
command,
|
||||
Command::new(vec![
|
||||
Part::Literal("cat".into()).spanned((0, 3)),
|
||||
Part::Concatenated(vec![
|
||||
Part::Literal("Hello ".into()).spanned((4, 11)),
|
||||
Part::ClosedSubshell(vec![Command::new(vec![
|
||||
Part::Literal("ls".into()).spanned((13, 15)),
|
||||
Part::Literal("-la".into()).spanned((16, 19)),
|
||||
])
|
||||
.spanned((13, 19)),])
|
||||
.spanned((11, 20)),
|
||||
])
|
||||
.spanned((4, 21)),
|
||||
])
|
||||
.spanned((0, 21))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse() {
|
||||
let source = r#"ls | rm -rf || touch 'hello.txt\' &
|
||||
cat "Hello $(ls -la)" && echo `ps \`; {echo Goodbye😀}"#;
|
||||
|
||||
let commands = Parser::new(Lexer::new(source, EscapeChar::Backslash, false))
|
||||
.parse()
|
||||
.commands;
|
||||
|
||||
assert_eq!(
|
||||
commands,
|
||||
[
|
||||
Command::new(vec![Part::Literal("ls".into()).spanned((0, 2))]).spanned((0, 3)),
|
||||
Command::new(vec![
|
||||
Part::Literal("rm".into()).spanned((5, 7)),
|
||||
Part::Literal("-rf".into()).spanned((8, 11))
|
||||
])
|
||||
.spanned((5, 12)),
|
||||
Command::new(vec![
|
||||
Part::Literal("touch".into()).spanned((15, 20)),
|
||||
Part::Literal("hello.txt\\".into()).spanned((21, 33))
|
||||
])
|
||||
.spanned((15, 34)),
|
||||
Command::new(vec![
|
||||
Part::Literal("cat".into()).spanned((36, 39)),
|
||||
Part::Concatenated(vec![
|
||||
Part::Literal("Hello ".into()).spanned((40, 47)),
|
||||
Part::ClosedSubshell(vec![Command::new(vec![
|
||||
Part::Literal("ls".into()).spanned((49, 51)),
|
||||
Part::Literal("-la".into()).spanned((52, 55)),
|
||||
])
|
||||
.spanned((49, 55))])
|
||||
.spanned((47, 56)),
|
||||
])
|
||||
.spanned((40, 57))
|
||||
])
|
||||
.spanned((36, 58)),
|
||||
Command::new(vec![
|
||||
Part::Literal("echo".into()).spanned((61, 65)),
|
||||
Part::ClosedSubshell(vec![Command::new(vec![
|
||||
Part::Literal("ps".into()).spanned((67, 69)),
|
||||
Part::Literal("\\".into()).spanned((70, 71)),
|
||||
])
|
||||
.spanned((67, 71))])
|
||||
.spanned((66, 72)),
|
||||
])
|
||||
.spanned((61, 72)),
|
||||
Command::new(vec![
|
||||
Part::Literal("echo".into()).spanned((75, 79)),
|
||||
Part::Literal("Goodbye😀".into()).spanned((80, 91))
|
||||
])
|
||||
.spanned((75, 91)),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
// Test that a backslash is retained when preceding a command.
|
||||
#[test]
|
||||
fn test_backslash_before_command() {
|
||||
let source = r#"\ls"#;
|
||||
let command = Parser::new(Lexer::new(source, EscapeChar::Backslash, false)).parse_command();
|
||||
|
||||
assert_eq!(
|
||||
command,
|
||||
Command::new(vec![Part::Literal(r"\ls".into()).spanned((0, 3))]).spanned((0, 3))
|
||||
);
|
||||
}
|
||||
|
||||
// Test that a backslash is not retained in the middle of a command.
|
||||
#[test]
|
||||
fn test_backslash_in_command() {
|
||||
let source = r#"ls \-la"#;
|
||||
let command = Parser::new(Lexer::new(source, EscapeChar::Backslash, false)).parse_command();
|
||||
|
||||
assert_eq!(
|
||||
command,
|
||||
Command::new(vec![
|
||||
Part::Literal("ls".into()).spanned((0, 2)),
|
||||
Part::Literal("-la".into()).spanned((3, 7))
|
||||
])
|
||||
.spanned((0, 7))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decompose_command() {
|
||||
let test_data = vec![
|
||||
("ls", vec!["ls"]),
|
||||
("$(ls)", vec!["ls"]),
|
||||
("ls -la", vec!["ls -la"]),
|
||||
("ls && cat", vec!["ls", "cat"]),
|
||||
(
|
||||
"ls $(foo | echo)",
|
||||
vec!["foo", "echo", "foo | echo", "ls $(foo | echo)"],
|
||||
),
|
||||
];
|
||||
|
||||
for (input, expected_output) in test_data {
|
||||
// Compare with hashsets bc we don't care about ordering.
|
||||
assert_eq!(
|
||||
HashSet::<String>::from_iter(decompose_command(input, EscapeChar::Backslash).0),
|
||||
HashSet::from_iter(expected_output.into_iter().map(ToString::to_string)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_command_without_leading_env_vars() {
|
||||
let test_data = vec![
|
||||
("X=1 rm -rf target", Some("rm -rf target")),
|
||||
(
|
||||
"X=1 Y=2 curl https://example.com",
|
||||
Some("curl https://example.com"),
|
||||
),
|
||||
("rm -rf target", Some("rm -rf target")),
|
||||
("X=1", None),
|
||||
];
|
||||
|
||||
for (input, expected_output) in test_data {
|
||||
assert_eq!(
|
||||
command_without_leading_env_vars(input, EscapeChar::Backslash),
|
||||
expected_output.map(ToString::to_string)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_contains_redirection() {
|
||||
let test_data = vec![
|
||||
("ls < \"file.txt < tmp\"", true),
|
||||
("echo $(ls > file.txt)", true),
|
||||
("ls >> file.txt", true),
|
||||
("ls < file.txt", true),
|
||||
("foo arg1 arg2 > file.txt", true),
|
||||
("foo && ls > file.txt", true),
|
||||
("echo \"hello world\" > output.txt", true),
|
||||
("echo \"5>4\"", false),
|
||||
("echo \"This message -> shows direction\"", false),
|
||||
("print(\"Value must be > 0 and < 100\")", false),
|
||||
];
|
||||
|
||||
for (cmd, should_contain_redirection) in test_data {
|
||||
let parser = Parser::new(Lexer::new(cmd, EscapeChar::Backslash, false));
|
||||
let contains_redirection = parser.parse().contains_redirection;
|
||||
assert_eq!(contains_redirection, should_contain_redirection);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_top_level_command() {
|
||||
let test_data = vec![
|
||||
("PAGER=0 git log", Some("git")),
|
||||
("PAGER= git log", Some("git")),
|
||||
("ls && git status", Some("ls")),
|
||||
("$(git status)", None),
|
||||
];
|
||||
|
||||
for (input, expected_output) in test_data {
|
||||
assert_eq!(
|
||||
top_level_command(input, EscapeChar::Backslash),
|
||||
expected_output.map(ToString::to_string)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,13 @@
|
||||
use galaxy_util::path::EscapeChar;
|
||||
use itertools::Itertools;
|
||||
|
||||
use crate::{
|
||||
parsers::{
|
||||
classify_command,
|
||||
hir::{CommandCallInfo, Flags, ShellCommand},
|
||||
simple::parse_for_completions,
|
||||
ClassifiedCommand,
|
||||
},
|
||||
signatures::testing::{create_test_command_registry, test_signature},
|
||||
};
|
||||
|
||||
use super::*;
|
||||
use crate::parsers::hir::{CommandCallInfo, Flags, ShellCommand};
|
||||
#[cfg(not(feature = "v2"))]
|
||||
use crate::parsers::hir::{Flag, FlagType};
|
||||
|
||||
use super::*;
|
||||
use crate::parsers::simple::parse_for_completions;
|
||||
use crate::parsers::{classify_command, ClassifiedCommand};
|
||||
use crate::signatures::testing::{create_test_command_registry, test_signature};
|
||||
|
||||
#[test]
|
||||
pub fn test_classify_command_classifies_known_command() {
|
||||
|
||||
@@ -1,19 +1,16 @@
|
||||
//! Contains the V2 implementation of internal command parsing logic that depends on the new,
|
||||
//! JS-compatible command signature struct (`crate::signatures::CommandSignature`).
|
||||
use super::hir::{self, Expression, Flags, ShellCommand};
|
||||
use super::{
|
||||
parse_arg, parse_dollar_expr, parse_unclassified_command, ArgumentError,
|
||||
FlagArgumentsCardinality, FlagSignature, LiteCommand, ParseError, ParsedExpression,
|
||||
ParsedToken,
|
||||
};
|
||||
use crate::completer::TopLevelCommandCaseSensitivity;
|
||||
use crate::meta::{HasSpan, Span, Spanned, SpannedItem};
|
||||
use crate::signatures::{
|
||||
get_matching_signature_for_tokenized_input, Command, CommandRegistry, Opt,
|
||||
};
|
||||
use crate::{
|
||||
completer::TopLevelCommandCaseSensitivity,
|
||||
meta::{HasSpan, Span, Spanned, SpannedItem},
|
||||
};
|
||||
|
||||
use super::parse_unclassified_command;
|
||||
use super::{
|
||||
hir::{self, Expression, Flags, ShellCommand},
|
||||
parse_arg, parse_dollar_expr, ArgumentError, FlagArgumentsCardinality, FlagSignature,
|
||||
LiteCommand, ParseError, ParsedExpression, ParsedToken,
|
||||
};
|
||||
|
||||
pub(super) fn parse_command(
|
||||
lite_cmd: &LiteCommand,
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
use crate::completer::{CommandExitStatus, CompletionContext, TopLevelCommandCaseSensitivity};
|
||||
use crate::parsers::SignatureAtTokenIndex;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use itertools::Itertools;
|
||||
use memo_map::MemoMap;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use warp_command_signatures::{Argument, DynamicCompletionData, IsArgumentOptional, Signature};
|
||||
|
||||
use crate::completer::{CommandExitStatus, CompletionContext, TopLevelCommandCaseSensitivity};
|
||||
use crate::parsers::SignatureAtTokenIndex;
|
||||
|
||||
pub enum SignatureResult<'a> {
|
||||
/// Successfully parsed the signature. We are returning the signature at the token to complete on.
|
||||
Success(SignatureAtTokenIndex<'a>),
|
||||
@@ -514,5 +514,5 @@ fn should_complete_on_subcmd(
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "registry_test.rs"]
|
||||
#[path = "registry_tests.rs"]
|
||||
mod tests;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use crate::completer::testing::FakeCompletionContext;
|
||||
use crate::completer::CompletionContext;
|
||||
use crate::completer::TopLevelCommandCaseSensitivity;
|
||||
use crate::completer::{CompletionContext, TopLevelCommandCaseSensitivity};
|
||||
use crate::signatures::registry::SignatureResult;
|
||||
use crate::signatures::testing::{create_test_command_registry, test_signature};
|
||||
|
||||
@@ -193,7 +192,7 @@ fn test_alias_expansion_path_skips_flag_with_value_before_subcommand() {
|
||||
let ctx = FakeCompletionContext::new(registry).with_case_sensitivity();
|
||||
|
||||
let result =
|
||||
galaxyui::r#async::block_on(ctx.command_registry().signature_with_alias_expansion(
|
||||
galaxyui_core::r#async::block_on(ctx.command_registry().signature_with_alias_expansion(
|
||||
&["kubectl", "-n", "default", "get"],
|
||||
true,
|
||||
&ctx,
|
||||
@@ -216,7 +215,7 @@ fn test_alias_expansion_path_skips_multiple_flags_before_subcommand() {
|
||||
let ctx = FakeCompletionContext::new(registry).with_case_sensitivity();
|
||||
|
||||
let result =
|
||||
galaxyui::r#async::block_on(ctx.command_registry().signature_with_alias_expansion(
|
||||
galaxyui_core::r#async::block_on(ctx.command_registry().signature_with_alias_expansion(
|
||||
&[
|
||||
"kubectl",
|
||||
"--context",
|
||||
|
||||
@@ -0,0 +1,235 @@
|
||||
use crate::completer::testing::FakeCompletionContext;
|
||||
use crate::completer::{CompletionContext, TopLevelCommandCaseSensitivity};
|
||||
use crate::signatures::registry::SignatureResult;
|
||||
use crate::signatures::testing::{create_test_command_registry, test_signature};
|
||||
|
||||
#[test]
|
||||
fn test_find_command_from_a_top_level_signature() {
|
||||
let bundle = warp_command_signatures::signature_by_name("bundle")
|
||||
.expect("global command signatures should include 'bundle'");
|
||||
|
||||
let registry = create_test_command_registry([bundle.clone(), test_signature()]);
|
||||
|
||||
let found_signature = registry
|
||||
.signature_from_line(
|
||||
"bundle exec ",
|
||||
TopLevelCommandCaseSensitivity::CaseSensitive,
|
||||
)
|
||||
.map(|c| c.signature);
|
||||
let signature = bundle;
|
||||
let exec_subcommand = signature
|
||||
.subcommands()
|
||||
.iter()
|
||||
.find(|sig| sig.name() == "exec");
|
||||
assert_eq!(found_signature, exec_subcommand);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_find_subcommand_signature_with_flags() {
|
||||
let kubectl = warp_command_signatures::signature_by_name("kubectl")
|
||||
.expect("global command signatures should include 'kubectl'");
|
||||
|
||||
let registry = create_test_command_registry([kubectl.clone(), test_signature()]);
|
||||
|
||||
let found_signature = registry
|
||||
.signature_from_line(
|
||||
"kubectl -n default get ",
|
||||
TopLevelCommandCaseSensitivity::CaseSensitive,
|
||||
)
|
||||
.expect("kubectl signature from line should exist");
|
||||
// Should parse this as entering the "get" subcommand even though there's a top level -n default flag.
|
||||
assert_eq!(found_signature.signature.name(), "get");
|
||||
assert_eq!(found_signature.token_index, 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_find_option_by_name_exact_match_does_not_match_substring() {
|
||||
// Regression test: "-n" should match the "-n"/"--namespace" option, NOT
|
||||
// "--no-headers" (which contains the substring "-n"). The fix uses exact
|
||||
// equality instead of `contains`.
|
||||
let kubectl = warp_command_signatures::signature_by_name("kubectl")
|
||||
.expect("global command signatures should include 'kubectl'");
|
||||
|
||||
let registry = create_test_command_registry([kubectl, test_signature()]);
|
||||
|
||||
// "kubectl -n default api-resources " should resolve to "api-resources",
|
||||
// which has a "--no-headers" option. If "-n" incorrectly matched
|
||||
// "--no-headers" via substring, the parser would skip "default" as the
|
||||
// flag argument and never reach the "api-resources" subcommand.
|
||||
let found_signature = registry
|
||||
.signature_from_line(
|
||||
"kubectl -n default api-resources ",
|
||||
TopLevelCommandCaseSensitivity::CaseSensitive,
|
||||
)
|
||||
.expect("kubectl signature from line should exist");
|
||||
assert_eq!(found_signature.signature.name(), "api-resources");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_flag_arg_consumes_token_matching_subcommand_name() {
|
||||
// When a recognized flag takes a required argument, the next token should be consumed
|
||||
// as that flag's argument even if it happens to match a subcommand name.
|
||||
// Here, --not-long takes 1 argument, so "one" is consumed as that argument
|
||||
// rather than being resolved as the "one" subcommand.
|
||||
let registry = create_test_command_registry([test_signature()]);
|
||||
|
||||
let found_signature = registry
|
||||
.signature_from_line(
|
||||
"test --not-long one foo ",
|
||||
TopLevelCommandCaseSensitivity::CaseSensitive,
|
||||
)
|
||||
.expect("test signature from line should exist");
|
||||
assert_eq!(found_signature.signature.name(), "test");
|
||||
assert_eq!(found_signature.token_index, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multiple_switches_before_subcommand() {
|
||||
// Multiple switch flags (no arguments) before a subcommand should all be
|
||||
// skipped, allowing the subcommand to be discovered.
|
||||
let registry = create_test_command_registry([test_signature()]);
|
||||
|
||||
let found_signature = registry
|
||||
.signature_from_line(
|
||||
"test -r -V one foo ",
|
||||
TopLevelCommandCaseSensitivity::CaseSensitive,
|
||||
)
|
||||
.expect("test signature from line should exist");
|
||||
assert_eq!(found_signature.signature.name(), "one");
|
||||
assert_eq!(found_signature.token_index, 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unrecognized_flag_skipped_before_subcommand() {
|
||||
// Unrecognized flags (tokens starting with '-' not in the spec) should be
|
||||
// skipped so the parser can still discover subcommands after them.
|
||||
let registry = create_test_command_registry([test_signature()]);
|
||||
|
||||
let found_signature = registry
|
||||
.signature_from_line(
|
||||
"test --unknown-flag one foo ",
|
||||
TopLevelCommandCaseSensitivity::CaseSensitive,
|
||||
)
|
||||
.expect("test signature from line should exist");
|
||||
assert_eq!(found_signature.signature.name(), "one");
|
||||
assert_eq!(found_signature.token_index, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_flag_with_missing_value_at_end_of_input() {
|
||||
// When a flag that takes a required argument appears at the end of input
|
||||
// with no value provided, the resolved signature stays on the parent command.
|
||||
let registry = create_test_command_registry([test_signature()]);
|
||||
|
||||
let found_signature = registry
|
||||
.signature_from_line(
|
||||
"test --not-long ",
|
||||
TopLevelCommandCaseSensitivity::CaseSensitive,
|
||||
)
|
||||
.expect("test signature from line should exist");
|
||||
assert_eq!(found_signature.signature.name(), "test");
|
||||
assert_eq!(found_signature.token_index, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multiple_flags_with_values_before_subcommand() {
|
||||
// Multiple valued flags before a subcommand should all be skipped,
|
||||
// allowing the subcommand to be discovered.
|
||||
let registry = create_test_command_registry([test_signature()]);
|
||||
|
||||
// --not-long takes 1 required arg ("val"), -r is a switch.
|
||||
let found_signature = registry
|
||||
.signature_from_line(
|
||||
"test --not-long val -r one foo ",
|
||||
TopLevelCommandCaseSensitivity::CaseSensitive,
|
||||
)
|
||||
.expect("test signature from line should exist");
|
||||
assert_eq!(found_signature.signature.name(), "one");
|
||||
assert_eq!(found_signature.token_index, 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_only_flags_no_subcommand() {
|
||||
// When only flags appear after the command with no following subcommand,
|
||||
// the parent command should be returned.
|
||||
let registry = create_test_command_registry([test_signature()]);
|
||||
|
||||
let found_signature = registry
|
||||
.signature_from_line(
|
||||
"test --not-long val ",
|
||||
TopLevelCommandCaseSensitivity::CaseSensitive,
|
||||
)
|
||||
.expect("test signature from line should exist");
|
||||
assert_eq!(found_signature.signature.name(), "test");
|
||||
assert_eq!(found_signature.token_index, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_optional_flag_arg_does_not_consume_subcommand() {
|
||||
// --required-and-optional-args has 1 required arg + 1 optional arg.
|
||||
// The parser should only skip the required arg, so "one" is found as a
|
||||
// subcommand rather than being consumed as the optional arg.
|
||||
let registry = create_test_command_registry([test_signature()]);
|
||||
|
||||
let found_signature = registry
|
||||
.signature_from_line(
|
||||
"test --required-and-optional-args val one foo ",
|
||||
TopLevelCommandCaseSensitivity::CaseSensitive,
|
||||
)
|
||||
.expect("test signature from line should exist");
|
||||
assert_eq!(found_signature.signature.name(), "one");
|
||||
assert_eq!(found_signature.token_index, 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_alias_expansion_path_skips_flag_with_value_before_subcommand() {
|
||||
// Exercises signature_with_alias_expansion (not just signature_from_tokens)
|
||||
// to ensure the alias-expansion code path also skips flags before subcommands.
|
||||
let kubectl = warp_command_signatures::signature_by_name("kubectl")
|
||||
.expect("global command signatures should include 'kubectl'");
|
||||
|
||||
let registry = create_test_command_registry([kubectl]);
|
||||
let ctx = FakeCompletionContext::new(registry).with_case_sensitivity();
|
||||
|
||||
let result =
|
||||
galaxyui_core::r#async::block_on(ctx.command_registry().signature_with_alias_expansion(
|
||||
&["kubectl", "-n", "default", "get"],
|
||||
true,
|
||||
&ctx,
|
||||
));
|
||||
let SignatureResult::Success(found_signature) = result else {
|
||||
panic!("expected SignatureResult::Success");
|
||||
};
|
||||
assert_eq!(found_signature.signature.name(), "get");
|
||||
assert_eq!(found_signature.token_index, 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_alias_expansion_path_skips_multiple_flags_before_subcommand() {
|
||||
// Exercises signature_with_alias_expansion with multiple flags (valued and
|
||||
// switch) placed before the subcommand.
|
||||
let kubectl = warp_command_signatures::signature_by_name("kubectl")
|
||||
.expect("global command signatures should include 'kubectl'");
|
||||
|
||||
let registry = create_test_command_registry([kubectl]);
|
||||
let ctx = FakeCompletionContext::new(registry).with_case_sensitivity();
|
||||
|
||||
let result =
|
||||
galaxyui_core::r#async::block_on(ctx.command_registry().signature_with_alias_expansion(
|
||||
&[
|
||||
"kubectl",
|
||||
"--context",
|
||||
"staging-cluster",
|
||||
"-n",
|
||||
"project1",
|
||||
"get",
|
||||
],
|
||||
true,
|
||||
&ctx,
|
||||
));
|
||||
let SignatureResult::Success(found_signature) = result else {
|
||||
panic!("expected SignatureResult::Success");
|
||||
};
|
||||
assert_eq!(found_signature.signature.name(), "get");
|
||||
assert_eq!(found_signature.token_index, 5);
|
||||
}
|
||||
@@ -5,13 +5,12 @@
|
||||
//! coverage can run with the "v2" Cargo feature both enabled and disabled.
|
||||
use galaxy_js::TypedJsFunctionRef;
|
||||
|
||||
use super::{TEST_GENERATOR_1_COMMAND, TEST_GENERATOR_2_COMMAND};
|
||||
use crate::signatures::{
|
||||
Argument, ArgumentValue, Arity, Command, CommandSignature, GeneratorFn, GeneratorResults,
|
||||
GeneratorScript, Opt, Priority, Suggestion, TemplateType,
|
||||
};
|
||||
|
||||
use super::{TEST_GENERATOR_1_COMMAND, TEST_GENERATOR_2_COMMAND};
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
pub(crate) static ref TEST_GENERATOR_1_JS_FUNCTION: TypedJsFunctionRef<String, GeneratorResults> = TypedJsFunctionRef::<String, GeneratorResults>::new_for_test();
|
||||
pub(crate) static ref TEST_GENERATOR_2_JS_FUNCTION: TypedJsFunctionRef<String, GeneratorResults> = TypedJsFunctionRef::<String, GeneratorResults>::new_for_test();
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
//! `galaxy_completer::signatures::CommandSignature`s, as well as `IntoWarpJs` implementations for
|
||||
//! Rust structs that may be passed to JS functions defined on the Command Signature (e.g.
|
||||
//! `GeneratorCompletionContext`).
|
||||
use galaxy_js::{
|
||||
util::{get_one_or_more_optional, get_one_or_more_required, get_optional, get_required},
|
||||
FromWarpJs, IntoWarpJs, JsFunctionRegistry,
|
||||
};
|
||||
use rquickjs::{FromJs, Function, Object, Value};
|
||||
use galaxy_js::util::{
|
||||
get_one_or_more_optional, get_one_or_more_required, get_optional, get_required,
|
||||
};
|
||||
use galaxy_js::{FromWarpJs, IntoWarpJs, JsFunctionRegistry};
|
||||
|
||||
use super::{
|
||||
Argument, ArgumentValue, Command, CommandSignature, GeneratorCompletionContext, GeneratorFn,
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
//! untokenized input.
|
||||
use itertools::Itertools;
|
||||
|
||||
use super::{registry::CommandRegistry, Command};
|
||||
use super::registry::CommandRegistry;
|
||||
use super::Command;
|
||||
|
||||
/// Returns the highest-precedence matching `Command` signature object for the given `input`, if
|
||||
/// any, along with the index of the token in `input` matched to the returned `Command`.
|
||||
@@ -144,5 +145,5 @@ fn deepest_matching_subcommand_signature<'a>(
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "lookup_test.rs"]
|
||||
#[path = "lookup_tests.rs"]
|
||||
mod test;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use crate::signatures::{Argument, Command, CommandSignature, Opt};
|
||||
|
||||
use super::*;
|
||||
use crate::signatures::{Argument, Command, CommandSignature, Opt};
|
||||
|
||||
/// Creates a `test_command` signature with a `test_subcommand` subcommand
|
||||
/// and the given options on the root command.
|
||||
|
||||
@@ -0,0 +1,417 @@
|
||||
use super::*;
|
||||
use crate::signatures::{Argument, Command, CommandSignature, Opt};
|
||||
|
||||
/// Creates a `test_command` signature with a `test_subcommand` subcommand
|
||||
/// and the given options on the root command.
|
||||
fn test_command_signature_with_options(options: Vec<Opt>) -> CommandSignature {
|
||||
CommandSignature {
|
||||
command: Command {
|
||||
name: "test_command".to_owned(),
|
||||
subcommands: vec![Command {
|
||||
name: "test_subcommand".to_owned(),
|
||||
..Default::default()
|
||||
}],
|
||||
options,
|
||||
..Default::default()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn valued_option() -> Opt {
|
||||
Opt {
|
||||
name: vec!["-n".to_owned(), "--name".to_owned()],
|
||||
arguments: vec![Argument {
|
||||
name: "value".to_owned(),
|
||||
..Default::default()
|
||||
}],
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_matching_signature_for_input_on_root_command() {
|
||||
let registry = CommandRegistry::new();
|
||||
registry.register_signature(CommandSignature {
|
||||
command: Command {
|
||||
name: "test_command".to_owned(),
|
||||
..Default::default()
|
||||
},
|
||||
});
|
||||
|
||||
let (found_signature, index) = get_matching_signature_for_input("test_command ", ®istry)
|
||||
.expect("Signature should exist");
|
||||
assert_eq!(found_signature.name, "test_command");
|
||||
assert_eq!(index, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_matching_signature_for_input_on_root_command_with_argument() {
|
||||
let registry = CommandRegistry::new();
|
||||
registry.register_signature(CommandSignature {
|
||||
command: Command {
|
||||
name: "test_command".to_owned(),
|
||||
subcommands: vec![Command {
|
||||
name: "test_subcommand".to_owned(),
|
||||
..Default::default()
|
||||
}],
|
||||
arguments: vec![Argument {
|
||||
name: "arg1".to_owned(),
|
||||
..Default::default()
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
});
|
||||
|
||||
let (found_signature, index) =
|
||||
get_matching_signature_for_input("test_command some_arg_value ", ®istry)
|
||||
.expect("Signature should be found.");
|
||||
assert_eq!(found_signature.name, "test_command");
|
||||
assert_eq!(index, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_matching_signature_for_input_on_subcommand() {
|
||||
let registry = CommandRegistry::new();
|
||||
registry.register_signature(CommandSignature {
|
||||
command: Command {
|
||||
name: "test_command".to_owned(),
|
||||
subcommands: vec![Command {
|
||||
name: "test_subcommand".to_owned(),
|
||||
..Default::default()
|
||||
}],
|
||||
arguments: vec![Argument {
|
||||
name: "arg1".to_owned(),
|
||||
..Default::default()
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
});
|
||||
|
||||
let (found_signature, index) =
|
||||
get_matching_signature_for_input("test_command test_subcommand ", ®istry)
|
||||
.expect("Signature should be found.");
|
||||
assert_eq!(found_signature.name, "test_subcommand");
|
||||
assert_eq!(index, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_matching_signature_for_input_on_subcommand_with_argument() {
|
||||
let registry = CommandRegistry::new();
|
||||
registry.register_signature(CommandSignature {
|
||||
command: Command {
|
||||
name: "test_command".to_owned(),
|
||||
subcommands: vec![
|
||||
Command {
|
||||
name: "test_subcommand1".to_owned(),
|
||||
arguments: vec![Argument {
|
||||
name: "test_subcommand_arg".to_owned(),
|
||||
..Default::default()
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
Command {
|
||||
name: "test_subcommand2".to_owned(),
|
||||
arguments: vec![Argument {
|
||||
name: "test_subcommand_arg".to_owned(),
|
||||
..Default::default()
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
],
|
||||
arguments: vec![Argument {
|
||||
name: "test_command_arg".to_owned(),
|
||||
..Default::default()
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
});
|
||||
|
||||
let (found_signature, index) = get_matching_signature_for_input(
|
||||
"test_command test_subcommand1 some_arg_value ",
|
||||
®istry,
|
||||
)
|
||||
.expect("Signature should be found.");
|
||||
assert_eq!(found_signature.name, "test_subcommand1");
|
||||
assert_eq!(index, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_matching_signature_for_input_without_trailing_whitespace() {
|
||||
let registry = CommandRegistry::new();
|
||||
registry.register_signature(CommandSignature {
|
||||
command: Command {
|
||||
name: "test_command".to_owned(),
|
||||
subcommands: vec![Command {
|
||||
name: "test_subcommand".to_owned(),
|
||||
..Default::default()
|
||||
}],
|
||||
arguments: vec![Argument {
|
||||
name: "arg1".to_owned(),
|
||||
..Default::default()
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
});
|
||||
|
||||
let (found_signature, index) =
|
||||
get_matching_signature_for_input("test_command test_subcommand", ®istry)
|
||||
.expect("Signature should be found.");
|
||||
|
||||
// The matched signature should be that of the top-level command. Because there is no trailing
|
||||
// whitespace in the input, it's assumed we're still completing on the "test_subcommand", so we
|
||||
// should still be using the top-level command signature.
|
||||
assert_eq!(found_signature.name, "test_command");
|
||||
assert_eq!(index, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_matching_signature_for_tokenized_input() {
|
||||
let registry = CommandRegistry::new();
|
||||
registry.register_signature(CommandSignature {
|
||||
command: Command {
|
||||
name: "test_command".to_owned(),
|
||||
subcommands: vec![
|
||||
Command {
|
||||
name: "test_subcommand1".to_owned(),
|
||||
arguments: vec![Argument {
|
||||
name: "test_subcommand_arg".to_owned(),
|
||||
..Default::default()
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
Command {
|
||||
name: "test_subcommand2".to_owned(),
|
||||
arguments: vec![Argument {
|
||||
name: "test_subcommand_arg".to_owned(),
|
||||
..Default::default()
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
],
|
||||
arguments: vec![Argument {
|
||||
name: "test_command_arg".to_owned(),
|
||||
..Default::default()
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
});
|
||||
|
||||
let (found_signature, token_index) = get_matching_signature_for_tokenized_input(
|
||||
&["test_command", "test_subcommand1", "some_arg_value"],
|
||||
true,
|
||||
®istry,
|
||||
)
|
||||
.expect("Signature should be found.");
|
||||
assert_eq!(found_signature.name, "test_subcommand1");
|
||||
assert_eq!(token_index, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_matching_signature_for_tokenized_input_without_trailing_whitespace() {
|
||||
let registry = CommandRegistry::new();
|
||||
registry.register_signature(CommandSignature {
|
||||
command: Command {
|
||||
name: "test_command".to_owned(),
|
||||
subcommands: vec![
|
||||
Command {
|
||||
name: "test_subcommand1".to_owned(),
|
||||
arguments: vec![Argument {
|
||||
name: "test_subcommand_arg".to_owned(),
|
||||
..Default::default()
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
Command {
|
||||
name: "test_subcommand2".to_owned(),
|
||||
arguments: vec![Argument {
|
||||
name: "test_subcommand_arg".to_owned(),
|
||||
..Default::default()
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
],
|
||||
arguments: vec![Argument {
|
||||
name: "test_command_arg".to_owned(),
|
||||
..Default::default()
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
});
|
||||
|
||||
let (found_signature, token_index) = get_matching_signature_for_tokenized_input(
|
||||
&["test_command", "test_subcommand1"],
|
||||
false,
|
||||
®istry,
|
||||
)
|
||||
.expect("Signature should be found.");
|
||||
|
||||
// The matched signature should be that of the top-level command. Because there is no trailing
|
||||
// whitespace in the input, it's assumed we're still completing on the "test_subcommand", so we
|
||||
// should still be using the top-level command signature.
|
||||
assert_eq!(found_signature.name, "test_command");
|
||||
assert_eq!(token_index, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_matching_signature_skips_flag_with_value_before_subcommand() {
|
||||
let registry = CommandRegistry::new();
|
||||
registry.register_signature(test_command_signature_with_options(vec![valued_option()]));
|
||||
|
||||
// -n takes a value, so the parser should skip "-n val" and find test_subcommand.
|
||||
let (found_signature, token_index) = get_matching_signature_for_tokenized_input(
|
||||
&["test_command", "-n", "val", "test_subcommand"],
|
||||
true,
|
||||
®istry,
|
||||
)
|
||||
.expect("Signature should be found.");
|
||||
assert_eq!(found_signature.name, "test_subcommand");
|
||||
assert_eq!(token_index, 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_matching_signature_skips_long_flag_with_value_before_subcommand() {
|
||||
let registry = CommandRegistry::new();
|
||||
registry.register_signature(test_command_signature_with_options(vec![
|
||||
Opt {
|
||||
name: vec!["--context".to_owned()],
|
||||
arguments: vec![Argument {
|
||||
name: "context".to_owned(),
|
||||
..Default::default()
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
valued_option(),
|
||||
]));
|
||||
|
||||
// Two valued flags before the subcommand should both be skipped.
|
||||
let (found_signature, token_index) = get_matching_signature_for_tokenized_input(
|
||||
&[
|
||||
"test_command",
|
||||
"--context",
|
||||
"staging",
|
||||
"-n",
|
||||
"project1",
|
||||
"test_subcommand",
|
||||
],
|
||||
true,
|
||||
®istry,
|
||||
)
|
||||
.expect("Signature should be found.");
|
||||
assert_eq!(found_signature.name, "test_subcommand");
|
||||
assert_eq!(token_index, 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_matching_signature_skips_switch_flag_before_subcommand() {
|
||||
let registry = CommandRegistry::new();
|
||||
registry.register_signature(test_command_signature_with_options(vec![Opt {
|
||||
name: vec!["--verbose".to_owned()],
|
||||
arguments: vec![],
|
||||
..Default::default()
|
||||
}]));
|
||||
|
||||
let (found_signature, token_index) = get_matching_signature_for_tokenized_input(
|
||||
&["test_command", "--verbose", "test_subcommand"],
|
||||
true,
|
||||
®istry,
|
||||
)
|
||||
.expect("Signature should be found.");
|
||||
assert_eq!(found_signature.name, "test_subcommand");
|
||||
assert_eq!(token_index, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_matching_signature_flag_at_end_without_value_does_not_panic() {
|
||||
let registry = CommandRegistry::new();
|
||||
registry.register_signature(test_command_signature_with_options(vec![valued_option()]));
|
||||
|
||||
// "-n" with no value should not panic.
|
||||
let (found_signature, token_index) =
|
||||
get_matching_signature_for_tokenized_input(&["test_command", "-n"], true, ®istry)
|
||||
.expect("Signature should be found.");
|
||||
assert_eq!(found_signature.name, "test_command");
|
||||
// No subcommand found, so entry_token_index (0) is returned.
|
||||
assert_eq!(token_index, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_matching_signature_skips_unrecognized_flag_before_subcommand() {
|
||||
// Unrecognized flags (tokens starting with '-' not in the spec) should be
|
||||
// skipped so the parser can still discover subcommands after them.
|
||||
let registry = CommandRegistry::new();
|
||||
registry.register_signature(test_command_signature_with_options(vec![]));
|
||||
|
||||
let (found_signature, token_index) = get_matching_signature_for_tokenized_input(
|
||||
&["test_command", "--unknown-flag", "test_subcommand"],
|
||||
true,
|
||||
®istry,
|
||||
)
|
||||
.expect("Signature should be found.");
|
||||
assert_eq!(found_signature.name, "test_subcommand");
|
||||
assert_eq!(token_index, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_matching_signature_flag_arg_consumes_token_matching_subcommand_name() {
|
||||
// When a recognized flag takes a required argument, the next token is
|
||||
// consumed as the flag's value even if it matches a subcommand name.
|
||||
let registry = CommandRegistry::new();
|
||||
registry.register_signature(test_command_signature_with_options(vec![valued_option()]));
|
||||
|
||||
let (found_signature, token_index) = get_matching_signature_for_tokenized_input(
|
||||
&["test_command", "-n", "test_subcommand", "extra"],
|
||||
true,
|
||||
®istry,
|
||||
)
|
||||
.expect("Signature should be found.");
|
||||
// "test_subcommand" was consumed as -n's value, so no subcommand is found.
|
||||
assert_eq!(found_signature.name, "test_command");
|
||||
assert_eq!(token_index, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_matching_signature_only_flags_no_subcommand() {
|
||||
// When the input consists only of flags with no following subcommand,
|
||||
// the parent command should be returned at the entry index.
|
||||
let registry = CommandRegistry::new();
|
||||
registry.register_signature(test_command_signature_with_options(vec![valued_option()]));
|
||||
|
||||
let (found_signature, token_index) =
|
||||
get_matching_signature_for_tokenized_input(&["test_command", "-n", "val"], true, ®istry)
|
||||
.expect("Signature should be found.");
|
||||
assert_eq!(found_signature.name, "test_command");
|
||||
assert_eq!(token_index, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_matching_signature_optional_flag_arg_does_not_consume_subcommand() {
|
||||
// A flag with 1 required + 1 optional argument should only skip the
|
||||
// required arg, so the next token can still match a subcommand.
|
||||
let registry = CommandRegistry::new();
|
||||
registry.register_signature(test_command_signature_with_options(vec![Opt {
|
||||
name: vec!["--output".to_owned()],
|
||||
arguments: vec![
|
||||
Argument {
|
||||
name: "format".to_owned(),
|
||||
..Default::default()
|
||||
},
|
||||
Argument {
|
||||
name: "extra".to_owned(),
|
||||
optional: true,
|
||||
..Default::default()
|
||||
},
|
||||
],
|
||||
..Default::default()
|
||||
}]));
|
||||
|
||||
// "json" is the required arg, "test_subcommand" should not be consumed as
|
||||
// the optional arg.
|
||||
let (found_signature, token_index) = get_matching_signature_for_tokenized_input(
|
||||
&["test_command", "--output", "json", "test_subcommand"],
|
||||
true,
|
||||
®istry,
|
||||
)
|
||||
.expect("Signature should be found.");
|
||||
assert_eq!(found_signature.name, "test_subcommand");
|
||||
assert_eq!(token_index, 3);
|
||||
}
|
||||
@@ -13,8 +13,6 @@ use std::cmp::Ordering;
|
||||
|
||||
pub use lookup::*;
|
||||
pub use registry::*;
|
||||
|
||||
use galaxy_js::TypedJsFunctionRef;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@@ -227,5 +225,5 @@ impl Opt {
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "signatures_test.rs"]
|
||||
#[path = "signatures_tests.rs"]
|
||||
mod test;
|
||||
|
||||
@@ -51,5 +51,5 @@ impl CommandRegistry {
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "registry_test.rs"]
|
||||
#[path = "registry_tests.rs"]
|
||||
mod test;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use crate::signatures::{Command, Priority};
|
||||
|
||||
use super::*;
|
||||
use crate::signatures::{Command, Priority};
|
||||
|
||||
fn test_signature() -> CommandSignature {
|
||||
CommandSignature {
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
use super::*;
|
||||
use crate::signatures::{Command, Priority};
|
||||
|
||||
fn test_signature() -> CommandSignature {
|
||||
CommandSignature {
|
||||
command: Command {
|
||||
name: "test".to_owned(),
|
||||
alias: vec![],
|
||||
description: None,
|
||||
arguments: vec![],
|
||||
subcommands: vec![],
|
||||
options: vec![],
|
||||
priority: Priority::default(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_command_registry_registers_signature() {
|
||||
let registry = CommandRegistry::new();
|
||||
registry.register_signature(test_signature());
|
||||
|
||||
let signature = registry
|
||||
.get_signature("test")
|
||||
.expect("Signature is registered.");
|
||||
assert_eq!(signature.command.name, "test");
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
use super::Priority;
|
||||
|
||||
#[test]
|
||||
fn test_priority_normalization() {
|
||||
let too_small = Priority::new(-201);
|
||||
assert_eq!(Priority::min(), too_small);
|
||||
|
||||
let too_large = Priority::new(201);
|
||||
assert_eq!(Priority::max(), too_large);
|
||||
|
||||
let fourty_two = Priority::new(42);
|
||||
assert_eq!(42, fourty_two.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_priority_comparison() {
|
||||
let super_important = Priority::new(200);
|
||||
let important = Priority::new(40);
|
||||
let not_important = Priority::new(-80);
|
||||
|
||||
assert!(super_important == super_important);
|
||||
assert!(super_important > important);
|
||||
assert!(super_important > not_important);
|
||||
|
||||
assert!(important == important);
|
||||
assert!(important > not_important);
|
||||
|
||||
assert!(not_important == not_important);
|
||||
}
|
||||
@@ -1,9 +1,8 @@
|
||||
use crate::{
|
||||
completer::{describe_given_token, CompletionContext},
|
||||
meta::{HasSpan as _, Span, SpannedItem},
|
||||
parsers::{simple::all_parsed_commands, LiteCommand},
|
||||
ParsedCommandsSnapshot, ParsedTokenData, ParsedTokensSnapshot,
|
||||
};
|
||||
use crate::completer::{describe_given_token, CompletionContext};
|
||||
use crate::meta::{HasSpan as _, Span, SpannedItem};
|
||||
use crate::parsers::simple::all_parsed_commands;
|
||||
use crate::parsers::LiteCommand;
|
||||
use crate::{ParsedCommandsSnapshot, ParsedTokenData, ParsedTokensSnapshot};
|
||||
|
||||
/// Parse the current commands in the editor's buffer and get descriptions
|
||||
/// for tokens within the commands. Note that this can be somewhat expensive,
|
||||
|
||||
Reference in New Issue
Block a user