first pass of merging in warp (doesn't build)

This commit is contained in:
Ryan Ward
2026-07-01 16:08:58 -05:00
parent 2f64909469
commit 4770ac06b5
3662 changed files with 414574 additions and 89772 deletions
+1 -1
View File
@@ -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);
}
+2 -3
View File
@@ -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 {}
+1 -1
View File
@@ -217,5 +217,5 @@ where
}
#[cfg(test)]
#[path = "meta_test.rs"]
#[path = "meta_tests.rs"]
mod tests;
+27
View File
@@ -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"]
+9 -13
View File
@@ -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.
+4 -8
View File
@@ -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)
);
}
}
+5 -12
View File
@@ -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() {
+8 -11
View File
@@ -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 ", &registry)
.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 ", &registry)
.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 ", &registry)
.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 ",
&registry,
)
.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", &registry)
.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,
&registry,
)
.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,
&registry,
)
.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,
&registry,
)
.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,
&registry,
)
.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,
&registry,
)
.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, &registry)
.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,
&registry,
)
.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,
&registry,
)
.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, &registry)
.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,
&registry,
)
.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);
}
+5 -6
View File
@@ -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,