Files
galaxy/crates/galaxy_completer/src/completer/describe_test.rs
T

537 lines
18 KiB
Rust

use std::collections::{HashMap, HashSet};
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};
#[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::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
);
}