Compare commits

...

33 Commits

Author SHA1 Message Date
Johannes Altmanninger
31363120aa build_tools/version-available-in-debian.sh: fix for BSD sed
Fixes https://github.com/fish-shell/fish-shell/pull/12651#issuecomment-4275827646
2026-04-20 09:57:42 +08:00
xtqqczze
2304077e0d gate benchmark feature on nightly toolchain
Closes #12653
2026-04-19 17:38:04 +08:00
xtqqczze
86c052b6ba fix non_upper_case_globals lint
Closes #12648
2026-04-19 17:37:41 +08:00
xtqqczze
68472da48a highlight: derive Display trait for HighlightRole
Closes #12645
2026-04-19 17:14:42 +08:00
Daniel D. Beck
4b172fc735 set_color: document output more prominently
Issue: https://github.com/fish-shell/fish-shell/issues/2378

Closes #12644
2026-04-19 17:14:42 +08:00
Nahor
944ab91fab tests: various fixes for Cygwin itself and ACL mounts
Most notably:
- Unlike MSYS, Cygwin seems to always properly handle symlinks (at least
in common scenarios)
- With ACL, "x" permission also requires "r" do to anything, be it files
or directories

Closes #12642
2026-04-19 17:09:36 +08:00
Johannes Altmanninger
34535fcb61 tests/checks/disown: fix signal delivery race
Intermittent test failure suggests that kill(3p) returns before the
signal is delivered.  Fix the failure by waiting until the signal
has been delivered before continuing the test.

Fixes #12635
2026-04-19 17:07:39 +08:00
Johannes Altmanninger
9e4eb37696 complete: remove stale comment
Commit a4b6348315 (clippy: fix collapsible_match lint, 2026-04-18) made
it so '$' characters are handled here, which contradicts the comment.
Remove it.
2026-04-19 15:55:13 +08:00
Johannes Altmanninger
dda76d7f18 Update to Rust 1.95 2026-04-19 15:53:36 +08:00
Johannes Altmanninger
fdb1d95521 updatecli.d/rust.yml: fix staleness check when using rustup 1.29 2026-04-19 15:53:08 +08:00
xtqqczze
937f3bc6cb Update to Rust 1.94 2026-04-19 00:17:30 +00:00
Daniel Rainer
ebc32adc09 clippy: fix map_unwrap_or lint
https://rust-lang.github.io/rust-clippy/rust-1.95.0/index.html#map_unwrap_or
2026-04-18 23:27:51 +00:00
xtqqczze
a4b6348315 clippy: fix collapsible_match lint
https://rust-lang.github.io/rust-clippy/rust-1.95.0/index.html#collapsible_match
2026-04-18 23:16:34 +00:00
xtqqczze
b21a4a7197 benchmark: fix unresolved import error
```rust
error[E0432]: unresolved import `crate::common::bytes2wcstring`
   --> src/common.rs:714:9
    |
714 |     use crate::common::bytes2wcstring;
    |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ no `bytes2wcstring` in `common`
```
2026-04-18 21:28:15 +00:00
xtqqczze
0cd227533f highlight: implement Display trait for HighlightRole 2026-04-17 20:27:24 -07:00
Johannes Altmanninger
5eb7687a64 tests/checks/tmux-complete4.fish: fix for macOS sed 2026-04-17 03:22:56 +08:00
Johannes Altmanninger
8d6426295e complete: automatically resolve REPLACES_TOKEN flag
This flag is implied by matches that require replacements.  Reflect that
in the Completion::new, reducing the number of places where we raise the
flag.  This slightly simplifies tasks like proving the parent commit.

There are other scenarios (e.g. wildcards) where we currently set
the flag additionally.
2026-04-17 01:31:29 +08:00
Johannes Altmanninger
85e76ba356 Fix option substr completions not being filtered out
Commit 3546ffa3ef (reader handle_completions(): remove dead filtering code,
2026-01-02) gives a proof of correctness that still makes sense;
The first lemma ("if will_replace_token") is trivially true, so no need to
assert it.
The second lemma ("if !will_replace_token") is violated in some edge cases:
we claim that given a token "-c", the option completion "--clip" is an exact match,
which is not true, it's a substring match.

Fix that, asserting the claim.
2026-04-17 01:31:29 +08:00
Johannes Altmanninger
fee4288122 complete: reuse string fuzzy match when completing ~$USER
If we get to this code path, we'll only get completions for user
names, so technically the full StringFuzzyMatch with its ranking of
samecase/smartcase/icase (only showing the best) might be overkill,
but it seems like a good idea to treat this the same way as other
completions.

The occasion for this commit is to correct a wrong
StringFuzzyMatch::exact_match() in the icase branch; which will be
important for a following commit.  Add a test for that.
2026-04-17 01:28:54 +08:00
Johannes Altmanninger
413246a93d reader handle_completions(): move loop-invariant code 2026-04-17 01:28:02 +08:00
Daniel Rainer
3cb939c9a8 fix: actually run without symlinks
The old behavior seems to have been introduced inadvertently:
https://github.com/fish-shell/fish-shell/pull/12636#issuecomment-4254328105

Closes #12636
2026-04-16 15:10:15 +08:00
Daniel Rainer
4790a444d8 lint: disable incorrect warning about unused fn
`cleanup` is used via `trap`.

Part of #12636
2026-04-16 15:10:15 +08:00
Daniel Rainer
da924927a0 cleanup: split up assignment and export
This prevents hiding failures of the `rustc` command.

Part of #12636
2026-04-16 15:10:15 +08:00
Daniel Rainer
29ff2fdd43 lint: disable warning about variable export
Here, we want `"$@"` to be expanded, since its components are the
arguments we want to pass to `export`.

Part of #12636
2026-04-16 15:10:14 +08:00
Daniel Rainer
732c04420b lint: disable warnings about desired behavior
We deliberately create subshells for the export in these cases, so we
don't want warnings about it.

Part of #12636
2026-04-16 15:10:14 +08:00
Daniel Rainer
947abd7464 cleanup: quote shell variables
This is not a functional change, since the variable names don't have
spaces, but it is more robust to changes and removes ShellCheck warnings

Part of #12636
2026-04-16 15:10:14 +08:00
Daniel Rainer
12cfe59578 fix: don't use echo -e in POSIX shell
The `-e` flag is not defined for `echo` in POSIX shell. Use `printf`
instead.

Part of #12636
2026-04-16 15:10:13 +08:00
Daniel Rainer
4b60d18b44 cleanup: remove unused variable
Part of #12636
2026-04-16 15:10:13 +08:00
Daniel Rainer
dd8e59db03 fix: check if system_tests args are empty
When `system_tests` is called without arguments, `[ -n "$@" ]` becomes
`[ -n ]`, which is true, resulting in running `export`, which lists all
exported variables, unnecessarily cluttering the output.
If `system_tests` is called with more than one argument, the check would
fail because having more than one argument after `-n` is invalid syntax.
Fix this by using `$*`, which concatenates all positional arguments to
`system_tests` into a single value.

Part of #12636
2026-04-16 15:10:13 +08:00
Johannes Altmanninger
47a3757f73 update changelog 2026-04-14 16:56:43 +08:00
Johannes Altmanninger
f278c29733 key: address "non_upper_case_globals" lint on named key constants 2026-04-14 16:56:43 +08:00
Peter Ammon
2bab8b5830 prompt_pwd: strip control characters
If a directory has a control sequence in it, then prompt_pwd (used in
the default prompt) would emit it to the console, which could cause
the terminal to interpret the escape sequence.

Strip control sequences from within prompt_pwd, in the same way as
we do in __fish_paste.fish, to sanitize it.

Closes #12629
2026-04-14 16:56:43 +08:00
Johannes Altmanninger
1dac221684 doc terminal_compatibility: tab title is OSC 1 2026-04-14 16:56:43 +08:00
40 changed files with 363 additions and 311 deletions

View File

@@ -25,7 +25,7 @@ runs:
set -x
toolchain=$(
case "$toolchain_channel" in
(stable) echo 1.93 ;; # updatecli.d/rust.yml
(stable) echo 1.95 ;; # updatecli.d/rust.yml
(msrv) echo 1.85 ;; # updatecli.d/rust.yml
(*)
printf >&2 "error: unsupported toolchain channel %s" "$toolchain_channel"

View File

@@ -21,4 +21,4 @@ jobs:
with:
command: check licenses
arguments: --all-features --locked --exclude-dev
rust-version: 1.93 # updatecli.d/rust.yml
rust-version: 1.95 # updatecli.d/rust.yml

View File

@@ -9,6 +9,8 @@ Deprecations and removed features
Interactive improvements
------------------------
- :doc:`prompt_pwd <cmds/prompt_pwd>` now strips control characters.
- Background color and underline color specified in :envvar:`fish_color_valid_path` are now respected (:issue:`12622`).
Improved terminal support
-------------------------
@@ -16,14 +18,16 @@ Improved terminal support
Other improvements
------------------
- History is no longer corrupted with NUL bytes when fish receives SIGTERM or SIGHUP (:issue:`10300`).
- ``fish_update_completions`` now handles groff ``\X'...'`` device control escapes, fixing completion generation for man pages produced by help2man 1.50 and later (such as coreutils 9.10).
- :doc:`fish_update_completions <cmds/fish_update_completions>` now handles groff ``\X'...'`` device control escapes, fixing completion generation for man pages produced by help2man 1.50 and later (such as coreutils 9.10).
For distributors and developers
-------------------------------
- When the default global config directory (``$PREFIX/etc/fish``) exists but has been overridden with ``-DCMAKE_INSTALL_SYSCONFDIR``, fish will now respect that override (:issue:`10748`).
- When the default global config directory (``$PREFIX/etc/fish``) exists but has been overridden via ``-DCMAKE_INSTALL_SYSCONFDIR``, fish will now respect that override (:issue:`10748`).
Regression fixes:
-----------------
- Vi mode ``dl`` (:issue:`12461`).
- (from 4.6) Backspace after newline (:issue:`12583`).
fish 4.6.0 (released March 28, 2026)
====================================

29
Cargo.lock generated
View File

@@ -296,7 +296,9 @@ dependencies = [
"rand",
"rsconf",
"rust-embed",
"rustc_version",
"serial_test",
"strum_macros",
"unix_path",
"xterm-color",
]
@@ -889,6 +891,15 @@ dependencies = [
"walkdir",
]
[[package]]
name = "rustc_version"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
dependencies = [
"semver",
]
[[package]]
name = "same-file"
version = "1.0.6"
@@ -919,6 +930,12 @@ version = "3.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "490dcfcbfef26be6800d11870ff2df8774fa6e86d047e3e8c8a76b25655e41ca"
[[package]]
name = "semver"
version = "1.0.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
[[package]]
name = "serde"
version = "1.0.228"
@@ -1015,6 +1032,18 @@ version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "strum_macros"
version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "syn"
version = "2.0.114"

View File

@@ -73,7 +73,9 @@ rust-embed = { version = "8.11.0", features = [
"include-exclude",
"interpolate-folder-path",
] }
rustc_version = "0.4.1"
serial_test = { version = "3", default-features = false }
strum_macros = "0.28.0"
widestring = "1.2.0"
unicode-segmentation = "1.12.0"
unicode-width = "0.2.0"
@@ -128,6 +130,7 @@ num-traits.workspace = true
once_cell.workspace = true
pcre2.workspace = true
rand.workspace = true
strum_macros.workspace = true
xterm-color.workspace = true
[target.'cfg(not(target_has_atomic = "64"))'.dependencies]
@@ -156,6 +159,7 @@ fish-build-helper.workspace = true
fish-gettext-mo-file-parser.workspace = true
phf_codegen = { workspace = true, optional = true }
rsconf.workspace = true
rustc_version.workspace = true
[target.'cfg(windows)'.build-dependencies]
unix_path.workspace = true
@@ -194,7 +198,6 @@ tsan = []
[workspace.lints]
rust.non_camel_case_types = "allow"
rust.non_upper_case_globals = "allow"
rust.unknown_lints = { level = "allow", priority = -1 }
rust.unstable_name_collisions = "allow"
rustdoc.private_intra_doc_links = "allow"

View File

@@ -6,6 +6,10 @@
use std::path::{Path, PathBuf};
fn main() {
let is_nightly =
rustc_version::version_meta().unwrap().channel == rustc_version::Channel::Nightly;
rsconf::declare_cfg("nightly", is_nightly);
setup_paths();
// Add our default to enable tools that don't go through CMake, like "cargo test" and the

View File

@@ -60,6 +60,7 @@ cargo() {
fi
}
# shellcheck disable=2329
cleanup () {
if [ -n "$gettext_template_dir" ] && [ -e "$gettext_template_dir" ]; then
rm -r "$gettext_template_dir"
@@ -89,6 +90,7 @@ fi
gettext_template_dir=$(mktemp -d)
(
# shellcheck disable=2030
export FISH_GETTEXT_EXTRACTION_DIR="$gettext_template_dir"
cargo build --workspace --all-targets --features=gettext-extract
)
@@ -112,7 +114,8 @@ fi
# - https://github.com/msys2/MSYS2-packages/issues/5784
(
if $is_cygwin; then
export PATH="$PATH:$(rustc --print target-libdir)"
PATH="$PATH:$(rustc --print target-libdir)"
export PATH
fi
cargo test --no-default-features --workspace --all-targets
)
@@ -124,20 +127,24 @@ fi
# Using "()" not "{}" because we do want a subshell (for the export)
system_tests() (
[ -n "$@" ] && export "$@"
# shellcheck disable=2163
[ -n "$*" ] && export "$@"
# shellcheck disable=2031
export FISH_GETTEXT_EXTRACTION_DIR="$gettext_template_dir"
"$workspace_root/tests/test_driver.py" "$build_dir"
)
test_cmd='FISH_GETTEXT_EXTRACTION_DIR="$gettext_template_dir" "$workspace_root/tests/test_driver.py" "$build_dir"'
if $is_cygwin; then
echo -e "=== Running ${green}integration tests ${yellow}with${green} symlinks${reset}"
system_tests $cygwin_var=winsymlinks
# shellcheck disable=2059
printf "=== Running ${green}integration tests ${yellow}with${green} symlinks${reset}\n"
system_tests "$cygwin_var"=winsymlinks
echo -e "=== Running ${green}integration tests ${yellow}without${green} symlinks${reset}"
system_tests $cygwin_var=winsymlinks
# shellcheck disable=2059
printf "=== Running ${green}integration tests ${yellow}without${green} symlinks${reset}\n"
system_tests "$cygwin_var"=
else
echo -e "=== Running ${green}integration tests${reset}"
# shellcheck disable=2059
printf "=== Running ${green}integration tests${reset}\n"
system_tests
fi

View File

@@ -11,6 +11,6 @@ codename=$(
curl -fsS https://sources.debian.org/api/src/"${package}"/ |
jq -r --arg codename "${codename}" '
.versions[] | select(.suites[] == $codename) | .version' |
sed 's/^\([0-9]\+\.[0-9]\+\).*/\1/' |
sed -E 's/^([0-9]+\.[0-9]+).*/\1/' |
sort --version-sort |
tail -1

View File

@@ -534,8 +534,8 @@ enum Mode {
let mut to_append_or_none = Some(c);
if mode == Mode::Unquoted {
match c {
'\\' => {
if !ignore_backslashes {
'\\'
if !ignore_backslashes => {
// Backslashes (escapes) are complicated and may result in errors, or
// appending INTERNAL_SEPARATORs, so we have to handle them specially.
if let Some(escape_chars) = read_unquoted_escape(
@@ -555,28 +555,25 @@ enum Mode {
// We've already appended, don't append anything else.
to_append_or_none = None;
}
}
'~' => {
'~'
if unescape_special
&& (input_position == 0 || Some(input_position) == potential_word_start)
{
=> {
to_append_or_none = Some(HOME_DIRECTORY);
}
}
'%' => {
'%'
// Note that this only recognizes %self if the string is literally %self.
// %self/foo will NOT match this.
if allow_percent_self
&& unescape_special
&& input_position == 0
&& input == PROCESS_EXPAND_SELF_STR
{
=> {
to_append_or_none = Some(PROCESS_EXPAND_SELF);
input_position += PROCESS_EXPAND_SELF_STR.len() - 1; // skip over 'self's
}
}
'*' => {
if unescape_special {
'*'
if unescape_special => {
// In general, this is ANY_STRING. But as a hack, if the last appended char
// is ANY_STRING, delete the last char and store ANY_STRING_RECURSIVE to
// reflect the fact that ** is the recursive wildcard.
@@ -588,14 +585,12 @@ enum Mode {
to_append_or_none = Some(ANY_STRING);
}
}
}
'?' => {
if unescape_special && !feature_test(FeatureFlag::QuestionMarkNoGlob) {
'?'
if unescape_special && !feature_test(FeatureFlag::QuestionMarkNoGlob) => {
to_append_or_none = Some(ANY_CHAR);
}
}
'$' => {
if unescape_special {
'$'
if unescape_special => {
let is_cmdsub = input_position + 1 < input.len()
&& input.char_at(input_position + 1) == '(';
if !is_cmdsub {
@@ -603,18 +598,16 @@ enum Mode {
vars_or_seps.push(input_position);
}
}
}
'{' => {
if unescape_special {
'{'
if unescape_special => {
brace_count += 1;
to_append_or_none = Some(BRACE_BEGIN);
// We need to store where the brace *ends up* in the output.
braces.push(result.len());
potential_word_start = Some(input_position + 1);
}
}
'}' => {
if unescape_special {
'}'
if unescape_special => {
// HACK: The completion machinery sometimes hands us partial tokens.
// We can't parse them properly, but it shouldn't hurt,
// so we don't assert here.
@@ -646,19 +639,16 @@ enum Mode {
}
}
}
}
',' => {
if unescape_special && brace_count > 0 {
','
if unescape_special && brace_count > 0 => {
to_append_or_none = Some(BRACE_SEP);
vars_or_seps.push(input_position);
potential_word_start = Some(input_position + 1);
}
}
' ' => {
if unescape_special && brace_count > 0 {
' '
if unescape_special && brace_count > 0 => {
to_append_or_none = Some(BRACE_SPACE);
}
}
'\'' => {
mode = Mode::SingleQuotes;
to_append_or_none = if unescape_special {
@@ -743,11 +733,9 @@ enum Mode {
}
}
}
'$' => {
if unescape_special {
to_append_or_none = Some(VARIABLE_EXPAND_SINGLE);
vars_or_seps.push(input_position);
}
'$' if unescape_special => {
to_append_or_none = Some(VARIABLE_EXPAND_SINGLE);
vars_or_seps.push(input_position);
}
_ => (),
}

View File

@@ -11,8 +11,10 @@ Synopsis
Description
-----------
``set_color`` is used to control the color and styling of text in the terminal.
*VALUE* describes that styling.
``set_color`` controls the color and styling of text in the terminal.
It writes non-printing color and text style escape sequences to standard output.
*VALUE* describes the styling.
*VALUE* can be a reserved color name like **red** or an RGB color value given as 3 or 6 hexadecimal digits ("F27" or "FF2277").
A special keyword **normal** resets text formatting to terminal defaults, however it is not recommended and the **--reset** option is preferred as it is less confusing and more future-proof.
@@ -93,7 +95,9 @@ Notes
3. Because of the risk of confusion, ``set_color --reset`` is recommended over ``set_color normal``.
4. Setting the background color only affects subsequently written characters. Fish provides no way to set the background color for the entire terminal window. Configuring the window background color (and other attributes such as its opacity) has to be done using whatever mechanisms the terminal provides. Look for a config option.
5. Some terminals use the ``--bold`` escape sequence to switch to a brighter color set rather than increasing the weight of text.
6. ``set_color`` works by printing sequences of characters to standard output. If used in command substitution or a pipe, these characters will also be captured. This may or may not be desirable. Checking the exit status of ``isatty stdout`` before using ``set_color`` can be useful to decide not to colorize output in a script.
6. If you use ``set_color`` in a command substitution or a pipe, these characters will also be captured.
This may or may not be desirable.
Checking the exit status of ``isatty stdout`` before using ``set_color`` can be useful to decide not to colorize output in a script.
Examples
--------

View File

@@ -237,7 +237,7 @@ Optional Commands
``\e]0; Pt \e\\``
- ts
- Set terminal window title (OSC 0). Used in :doc:`fish_title <cmds/fish_title>`.
* - ``\e]2; Pt \e\\``
* - ``\e]1; Pt \e\\``
- ts
- Set terminal tab title (OSC 1). Used in :doc:`fish_tab_title <cmds/fish_tab_title>`.
* - ``\e]7;file:// Pt / Pt \e\\``

View File

@@ -26,7 +26,8 @@ function prompt_pwd --description 'short CWD for the prompt'
or set -l fish_prompt_pwd_full_dirs 1
for path in $argv
set -l tmp (__fish_unexpand_tilde $path)
# Strip control characters to avoid injecting terminal escape sequences into the prompt.
set -l tmp (__fish_unexpand_tilde $path | string replace -ra '[[:cntrl:]]' '')
if test "$fish_prompt_pwd_dir_length" -eq 0
echo $tmp

View File

@@ -2889,7 +2889,7 @@ fn test_is_same_node() {
}
// Run with cargo +nightly bench --features=benchmark
#[cfg(feature = "benchmark")]
#[cfg(all(nightly, feature = "benchmark"))]
#[cfg(test)]
mod bench {
extern crate test;

View File

@@ -117,7 +117,7 @@ fn generate_output_string(seq: &[Key], user: bool, bind: &InputMapping) -> WStri
if key.modifiers == Modifiers::ALT {
out.push_utfstr(&char_to_symbol('\x1b', i == 0));
out.push_utfstr(&char_to_symbol(
if key.codepoint == key::Escape {
if key.codepoint == key::ESCAPE {
'\x1b'
} else {
key.codepoint

View File

@@ -1206,39 +1206,6 @@ fn read_file(mut f: impl Read) -> Result<WString, ()> {
Ok(bytes2wcstring(&buf))
}
fn highlight_role_to_string(role: HighlightRole) -> &'static wstr {
match role {
HighlightRole::normal => L!("normal"),
HighlightRole::error => L!("error"),
HighlightRole::command => L!("command"),
HighlightRole::keyword => L!("keyword"),
HighlightRole::statement_terminator => L!("statement_terminator"),
HighlightRole::param => L!("param"),
HighlightRole::option => L!("option"),
HighlightRole::comment => L!("comment"),
HighlightRole::search_match => L!("search_match"),
HighlightRole::operat => L!("operat"),
HighlightRole::escape => L!("escape"),
HighlightRole::quote => L!("quote"),
HighlightRole::redirection => L!("redirection"),
HighlightRole::autosuggestion => L!("autosuggestion"),
HighlightRole::selection => L!("selection"),
HighlightRole::pager_progress => L!("pager_progress"),
HighlightRole::pager_background => L!("pager_background"),
HighlightRole::pager_prefix => L!("pager_prefix"),
HighlightRole::pager_completion => L!("pager_completion"),
HighlightRole::pager_description => L!("pager_description"),
HighlightRole::pager_secondary_background => L!("pager_secondary_background"),
HighlightRole::pager_secondary_prefix => L!("pager_secondary_prefix"),
HighlightRole::pager_secondary_completion => L!("pager_secondary_completion"),
HighlightRole::pager_secondary_description => L!("pager_secondary_description"),
HighlightRole::pager_selected_background => L!("pager_selected_background"),
HighlightRole::pager_selected_prefix => L!("pager_selected_prefix"),
HighlightRole::pager_selected_completion => L!("pager_selected_completion"),
HighlightRole::pager_selected_description => L!("pager_selected_description"),
}
}
// Entry point for Pygments CSV output.
// Our output is a newline-separated string.
// Each line is of the form `start,end,role`
@@ -1281,14 +1248,7 @@ struct TokenRange {
// Now render these to a string.
let mut result = String::with_capacity(token_ranges.len() * 32);
for range in token_ranges {
writeln!(
result,
"{},{},{}",
range.start,
range.end,
highlight_role_to_string(range.role)
)
.unwrap();
writeln!(result, "{},{},{}", range.start, range.end, range.role).unwrap();
}
result.into_bytes()
}

View File

@@ -5,8 +5,8 @@
use crate::{builtins::error::Error, env::Environment as _, err_fmt, wutil::wrealpath};
// The pwd builtin. Respect -P to resolve symbolic links. Respect -L to not do that (the default).
const short_options: &wstr = L!("LPh");
const long_options: &[WOption] = &[
const SHORT_OPTIONS: &wstr = L!("LPh");
const LONG_OPTIONS: &[WOption] = &[
wopt(L!("help"), NoArgument, 'h'),
wopt(L!("logical"), NoArgument, 'L'),
wopt(L!("physical"), NoArgument, 'P'),
@@ -16,7 +16,7 @@ pub fn pwd(parser: &Parser, streams: &mut IoStreams, argv: &mut [&wstr]) -> Buil
let cmd = argv[0];
let argc = argv.len();
let mut resolve_symlinks = false;
let mut w = WGetopter::new(short_options, long_options, argv);
let mut w = WGetopter::new(SHORT_OPTIONS, LONG_OPTIONS, argv);
while let Some(opt) = w.next_opt() {
match opt {
'L' => resolve_symlinks = false,

View File

@@ -16,8 +16,8 @@ struct Options {
no_symlinks: bool,
}
const short_options: &wstr = L!("+hs");
const long_options: &[WOption] = &[
const SHORT_OPTIONS: &wstr = L!("+hs");
const LONG_OPTIONS: &[WOption] = &[
wopt(L!("no-symlinks"), NoArgument, 's'),
wopt(L!("help"), NoArgument, 'h'),
];
@@ -31,7 +31,7 @@ fn parse_options(
let mut opts = Options::default();
let mut w = WGetopter::new(short_options, long_options, args);
let mut w = WGetopter::new(SHORT_OPTIONS, LONG_OPTIONS, args);
while let Some(c) = w.next_opt() {
match c {

View File

@@ -286,7 +286,7 @@ pub fn is_windows_subsystem_for_linux(v: WSL) -> bool {
Some(WSL::V1)
});
wsl.map(|wsl| v == WSL::Any || wsl == v).unwrap_or(false)
wsl.is_some_and(|wsl| v == WSL::Any || wsl == v)
}
/// Test if the given char is valid in a variable name.
@@ -707,11 +707,11 @@ macro_rules! check_decode {
}
}
#[cfg(feature = "benchmark")]
#[cfg(all(nightly, feature = "benchmark"))]
#[cfg(test)]
mod bench {
extern crate test;
use crate::common::bytes2wcstring;
use fish_widestring::bytes2wcstring;
use test::Bencher;
#[bench]

View File

@@ -32,8 +32,7 @@
use fish_util::wcsfilecmp;
use fish_wcstringutil::{
StringFuzzyMatch, string_fuzzy_match_string, string_prefixes_string,
string_prefixes_string_case_insensitive, string_suffixes_string_case_insensitive,
strip_executable_suffix,
string_suffixes_string_case_insensitive, strip_executable_suffix,
};
use fish_widestring::{WExt as _, charptr2wcstring};
use std::{
@@ -159,7 +158,10 @@ pub fn new(
r#match: StringFuzzyMatch, /* = exact_match */
flags: CompleteFlags,
) -> Self {
let flags = resolve_auto_space(&completion, flags);
let mut flags = resolve_auto_space(&completion, flags);
if r#match.requires_full_replacement() {
flags |= CompleteFlags::REPLACES_TOKEN;
}
Self {
completion,
description,
@@ -1483,14 +1485,12 @@ fn complete_param_for_command(
continue;
};
let mut offset = 0;
let mut flags = CompleteFlags::empty();
if r#match.requires_full_replacement() {
flags = CompleteFlags::REPLACES_TOKEN;
let offset = if r#match.requires_full_replacement() {
0
} else {
offset = s.len();
}
s.len()
};
let completion = whole_opt.slice_from(offset);
// does this switch have any known arguments
let has_arg = !o.comp.is_empty();
@@ -1502,14 +1502,14 @@ fn complete_param_for_command(
// a completion. By default we avoid using '=' and instead rely on '--switch
// switch-arg', since it is more commonly supported by homebrew getopt-like
// functions.
let completion = sprintf!("%s=", whole_opt.slice_from(offset));
let completion = sprintf!("%s=", completion);
// Append a long-style option with a mandatory trailing equal sign
if !self.completions.add(Completion::new(
completion,
o.desc.localize().to_owned(),
StringFuzzyMatch::exact_match(),
flags | CompleteFlags::NO_SPACE,
r#match,
CompleteFlags::NO_SPACE,
)) {
return false;
}
@@ -1517,10 +1517,10 @@ fn complete_param_for_command(
// Append a long-style option
if !self.completions.add(Completion::new(
whole_opt.slice_from(offset).to_owned(),
completion.to_owned(),
o.desc.localize().to_owned(),
StringFuzzyMatch::exact_match(),
flags,
r#match,
CompleteFlags::empty(),
)) {
return false;
}
@@ -1666,7 +1666,7 @@ fn complete_variable(&mut self, s: &wstr, start_offset: usize) -> bool {
// Take only the suffix.
env_name.slice_from(varlen).to_owned()
} else {
flags |= CompleteFlags::REPLACES_TOKEN | CompleteFlags::DONT_ESCAPE;
flags |= CompleteFlags::DONT_ESCAPE;
whole_var.slice_to(start_offset).to_owned() + env_name.as_utfstr()
};
@@ -1735,10 +1735,8 @@ enum Mode {
match c {
'\\' => skip_next = true,
'$' => {
if mode == Unquoted || mode == DoubleQuoted {
variable_start = Some(in_pos);
}
'$' if (mode == Unquoted || mode == DoubleQuoted) => {
variable_start = Some(in_pos);
}
'\'' => {
if mode == SingleQuoted {
@@ -1754,9 +1752,7 @@ enum Mode {
mode = DoubleQuoted;
}
}
_ => {
// all other chars ignored here
}
_ => (),
}
}
@@ -1823,30 +1819,23 @@ fn getpwent_name() -> Option<WString> {
break;
}
if string_prefixes_string(user_name, &pw_name) {
if let Some(r#match) = StringFuzzyMatch::try_create(user_name, &pw_name, true) {
let desc = wgettext_fmt!(COMPLETE_USER_DESC, &pw_name);
// Append a user name.
// TODO: propagate overflow?
let mut flags = CompleteFlags::NO_SPACE;
if r#match.requires_full_replacement() {
flags |= CompleteFlags::DONT_ESCAPE;
}
let _ = self.completions.add(Completion::new(
pw_name.slice_from(name_len).to_owned(),
if r#match.requires_full_replacement() {
sprintf!("~%s", &pw_name)
} else {
pw_name.slice_from(name_len).to_owned()
},
desc,
StringFuzzyMatch::exact_match(),
CompleteFlags::NO_SPACE,
));
result = true;
} else if string_prefixes_string_case_insensitive(user_name, &pw_name) {
let name = sprintf!("~%s", &pw_name);
let desc = wgettext_fmt!(COMPLETE_USER_DESC, &pw_name);
// Append a user name
// TODO: propagate overflow?
let _ = self.completions.add(Completion::new(
name,
desc,
StringFuzzyMatch::exact_match(),
CompleteFlags::REPLACES_TOKEN
| CompleteFlags::DONT_ESCAPE
| CompleteFlags::NO_SPACE,
r#match,
flags,
));
result = true;
}

View File

@@ -800,10 +800,8 @@ fn expand_braces(
brace_end = Some(pos);
}
}
BRACE_SEP => {
if brace_count == 1 {
last_sep = Some(pos);
}
BRACE_SEP if brace_count == 1 => {
last_sep = Some(pos);
}
_ => {
// we ignore all other characters here

View File

@@ -5,7 +5,7 @@
use crate::exec::{PgroupPolicy, is_thompson_shell_script};
use crate::proc::Job;
use crate::redirection::Dup2List;
use crate::signal::signals_to_default;
use crate::signal::SIGNALS_TO_DEFAULT;
use errno::Errno;
use libc::{c_char, posix_spawn_file_actions_t, posix_spawnattr_t};
use std::ffi::{CStr, CString};
@@ -129,7 +129,7 @@ pub fn new(
}
// Everybody gets default handlers.
attr.set_sigdefault(&signals_to_default)?;
attr.set_sigdefault(&SIGNALS_TO_DEFAULT)?;
// Reset the sigmask.
let mut sigmask = MaybeUninit::uninit();

View File

@@ -34,6 +34,7 @@
WString, wstr,
};
use std::collections::{HashMap, hash_map::Entry};
use strum_macros::Display;
use super::file_tester::IsFile;
@@ -586,10 +587,8 @@ enum Mode {
} else {
// Not a backslash.
match c {
'~' => {
if in_pos == 0 {
colors[in_pos] = HighlightSpec::with_fg(HighlightRole::operat);
}
'~' if in_pos == 0 => {
colors[in_pos] = HighlightSpec::with_fg(HighlightRole::operat);
}
'$' => {
assert!(in_pos < buff_len);
@@ -597,10 +596,8 @@ enum Mode {
// Subtract one to account for the upcoming loop increment.
in_pos -= 1;
}
'?' => {
if !feature_test(FeatureFlag::QuestionMarkNoGlob) {
colors[in_pos] = HighlightSpec::with_fg(HighlightRole::operat);
}
'?' if !feature_test(FeatureFlag::QuestionMarkNoGlob) => {
colors[in_pos] = HighlightSpec::with_fg(HighlightRole::operat);
}
'*' | '(' | ')' => {
colors[in_pos] = HighlightSpec::with_fg(HighlightRole::operat);
@@ -613,10 +610,8 @@ enum Mode {
colors[in_pos] = HighlightSpec::with_fg(HighlightRole::operat);
bracket_count -= 1;
}
',' => {
if bracket_count > 0 {
colors[in_pos] = HighlightSpec::with_fg(HighlightRole::operat);
}
',' if bracket_count > 0 => {
colors[in_pos] = HighlightSpec::with_fg(HighlightRole::operat);
}
'\'' => {
colors[in_pos] = HighlightSpec::with_fg(HighlightRole::quote);
@@ -660,9 +655,9 @@ enum Mode {
'"' => {
mode = Mode::unquoted;
}
'\\' => {
'\\'
// Backslash
if in_pos + 1 < buff_len {
if in_pos + 1 < buff_len => {
let escaped_char = buffstr.as_char_slice()[in_pos + 1];
if matches!(escaped_char, '\\' | '"' | '\n' | '$') {
colors[in_pos] = HighlightSpec::with_fg(HighlightRole::escape); // backslash
@@ -670,7 +665,6 @@ enum Mode {
in_pos += 1; // skip over backslash
}
}
}
'$' => {
in_pos += color_variable(&buffstr[in_pos..], &mut colors[in_pos..]);
// Subtract one to account for the upcoming increment in the loop.
@@ -1264,7 +1258,7 @@ fn get_fallback(role: HighlightRole) -> HighlightRole {
}
/// Describes the role of a span of text.
#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq, Display)]
#[repr(u8)]
pub enum HighlightRole {
#[default]

View File

@@ -361,19 +361,19 @@ pub fn init_input() {
};
add(vec![], "self-insert");
add(vec![Key::from_raw(key::Enter)], "execute");
add(vec![Key::from_raw(key::Tab)], "complete");
add(vec![Key::from_raw(key::ENTER)], "execute");
add(vec![Key::from_raw(key::TAB)], "complete");
add(vec![ctrl('c')], "cancel-commandline");
add(vec![ctrl('d')], "exit");
add(vec![ctrl('e')], "bind");
add(vec![ctrl('s')], "pager-toggle-search");
add(vec![ctrl('u')], "backward-kill-line");
add(vec![Key::from_raw(key::Backspace)], "backward-delete-char");
add(vec![Key::from_raw(key::BACKSPACE)], "backward-delete-char");
// Arrows - can't have functions, so *-or-search isn't available.
add(vec![Key::from_raw(key::Up)], "up-line");
add(vec![Key::from_raw(key::Down)], "down-line");
add(vec![Key::from_raw(key::Right)], "forward-char");
add(vec![Key::from_raw(key::Left)], "backward-char");
add(vec![Key::from_raw(key::UP)], "up-line");
add(vec![Key::from_raw(key::DOWN)], "down-line");
add(vec![Key::from_raw(key::RIGHT)], "forward-char");
add(vec![Key::from_raw(key::LEFT)], "backward-char");
// Emacs style
add(vec![ctrl('p')], "up-line");
add(vec![ctrl('n')], "down-line");
@@ -585,11 +585,11 @@ fn try_peek_sequence(
!seq.is_empty(),
"Empty sequence passed to try_peek_sequence"
);
let mut prev = Key::from_raw(key::Invalid);
let mut prev = Key::from_raw(key::INVALID);
for key in seq {
// If we just read an escape, we need to add a timeout for the next char,
// to distinguish between the actual escape key and an "alt"-modifier.
let escaped = *style != KeyNameStyle::Plain && prev == Key::from_raw(key::Escape);
let escaped = *style != KeyNameStyle::Plain && prev == Key::from_raw(key::ESCAPE);
let Some(spec) = self.next_is_char(style, *key, escaped) else {
return false;
};
@@ -650,7 +650,7 @@ struct MatchedMapping<'a> {
if self.try_peek_sequence(&m.key_name_style, &m.seq, &mut quality) {
// // A binding for just escape should also be deferred
// // so escape sequences take precedence.
let is_escape = m.seq == vec![Key::from_raw(key::Escape)];
let is_escape = m.seq == vec![Key::from_raw(key::ESCAPE)];
let is_perfect_match = quality
.iter()
.all(|key_match| *key_match == KeyMatchQuality::Exact);

View File

@@ -200,13 +200,13 @@ pub(crate) fn codepoint_text(&self) -> Option<char> {
if modifiers.is_some() {
return None;
}
if c == key::Space {
if c == key::SPACE {
return Some(' ');
}
if c == key::Enter {
if c == key::ENTER {
return Some('\n');
}
if c == key::Tab {
if c == key::TAB {
return Some('\t');
}
if fish_is_pua(c) || u32::from(c) <= 27 {
@@ -817,10 +817,10 @@ fn readch(&mut self) -> CharEvent {
continue;
}
let mut seq = WString::new();
if key.is_some_and(|key| key.key == Key::from_raw(key::Invalid)) {
if key.is_some_and(|key| key.key == Key::from_raw(key::INVALID)) {
continue;
}
assert!(key.is_none_or(|key| key.codepoint != key::Invalid));
assert!(key.is_none_or(|key| key.codepoint != key::INVALID));
// At this point, the bytes in `buffer` should be parsed as a UTF-8 sequence,
// or, if they are not valid UTF-8, ignored. On incomplete sequences, another
// byte is read and decoding is tried again in the next iteration.
@@ -953,15 +953,15 @@ fn parse_escape_sequence(
assert!(buffer.len() <= 2);
let recursive_invocation = buffer.len() == 2;
let Some(next) = self.read_sequence_byte(buffer) else {
return Some(KeyEvent::from_raw(key::Escape));
return Some(KeyEvent::from_raw(key::ESCAPE));
};
let invalid = KeyEvent::from_raw(key::Invalid);
let invalid = KeyEvent::from_raw(key::INVALID);
if recursive_invocation && next == b'\x1b' {
return Some(
match self.parse_escape_sequence(buffer, have_escape_prefix) {
Some(mut nested_sequence) => {
if nested_sequence.key == invalid.key {
return Some(KeyEvent::from_raw(key::Escape));
return Some(KeyEvent::from_raw(key::ESCAPE));
}
nested_sequence.modifiers.alt = true;
nested_sequence
@@ -1085,13 +1085,13 @@ fn parse_csi(&mut self, buffer: &mut Vec<u8>) -> Option<KeyEvent> {
_ => return None,
}
}
b'A' => masked_key(key::Up),
b'B' => masked_key(key::Down),
b'C' => masked_key(key::Right),
b'D' => masked_key(key::Left),
b'A' => masked_key(key::UP),
b'B' => masked_key(key::DOWN),
b'C' => masked_key(key::RIGHT),
b'D' => masked_key(key::LEFT),
b'E' => masked_key('5'), // Numeric keypad
b'F' => masked_key(key::End), // PC/xterm style
b'H' => masked_key(key::Home), // PC/xterm style
b'F' => masked_key(key::END), // PC/xterm style
b'H' => masked_key(key::HOME), // PC/xterm style
b'M' | b'm' => {
flog!(reader, "mouse event");
// Generic X10 or modified VT200 sequence, or extended (SGR/1006) mouse
@@ -1169,14 +1169,14 @@ fn parse_csi(&mut self, buffer: &mut Vec<u8>) -> Option<KeyEvent> {
}
b'S' => masked_key(function_key(4)),
b'~' => match params[0][0] {
1 => masked_key(key::Home), // VT220/tmux style
2 => masked_key(key::Insert),
3 => masked_key(key::Delete),
4 => masked_key(key::End), // VT220/tmux style
5 => masked_key(key::PageUp),
6 => masked_key(key::PageDown),
7 => masked_key(key::Home), // rxvt style
8 => masked_key(key::End), // rxvt style
1 => masked_key(key::HOME), // VT220/tmux style
2 => masked_key(key::INSERT),
3 => masked_key(key::DELETE),
4 => masked_key(key::END), // VT220/tmux style
5 => masked_key(key::PAGE_UP),
6 => masked_key(key::PAGE_DOWN),
7 => masked_key(key::HOME), // rxvt style
8 => masked_key(key::END), // rxvt style
11..=15 => masked_key(
char::from_u32(u32::from(function_key(1)) + params[0][0] - 11).unwrap(),
),
@@ -1236,8 +1236,8 @@ fn parse_csi(&mut self, buffer: &mut Vec<u8>) -> Option<KeyEvent> {
// Treat numpad keys the same as their non-numpad counterparts. Could add a numpad modifier here.
let key = match params[0][0] {
57361 => key::PrintScreen,
57363 => key::Menu,
57361 => key::PRINT_SCREEN,
57363 => key::MENU,
57399 => '0',
57400 => '1',
57401 => '2',
@@ -1253,18 +1253,18 @@ fn parse_csi(&mut self, buffer: &mut Vec<u8>) -> Option<KeyEvent> {
57411 => '*',
57412 => '-',
57413 => '+',
57414 => key::Enter,
57414 => key::ENTER,
57415 => '=',
57417 => key::Left,
57418 => key::Right,
57419 => key::Up,
57420 => key::Down,
57421 => key::PageUp,
57422 => key::PageDown,
57423 => key::Home,
57424 => key::End,
57425 => key::Insert,
57426 => key::Delete,
57417 => key::LEFT,
57418 => key::RIGHT,
57419 => key::UP,
57420 => key::DOWN,
57421 => key::PAGE_UP,
57422 => key::PAGE_DOWN,
57423 => key::HOME,
57424 => key::END,
57425 => key::INSERT,
57426 => key::DELETE,
cp => {
let Some(key) = char::from_u32(cp) else {
return invalid_sequence(buffer);
@@ -1284,7 +1284,7 @@ fn parse_csi(&mut self, buffer: &mut Vec<u8>) -> Option<KeyEvent> {
Some(base_layout_key),
)
}
b'Z' => KeyEvent::from(shift(key::Tab)),
b'Z' => KeyEvent::from(shift(key::TAB)),
b'I' => {
self.push_front(CharEvent::Implicit(ImplicitEvent::FocusIn));
return None;
@@ -1310,15 +1310,15 @@ fn parse_ss3(&mut self, buffer: &mut Vec<u8>) -> Option<KeyEvent> {
let (modifiers, _caps_lock) = parse_mask(raw_mask.saturating_sub(1));
#[rustfmt::skip]
let key = match code {
b' ' => KeyEvent::new(modifiers, key::Space),
b'A' => KeyEvent::new(modifiers, key::Up),
b'B' => KeyEvent::new(modifiers, key::Down),
b'C' => KeyEvent::new(modifiers, key::Right),
b'D' => KeyEvent::new(modifiers, key::Left),
b'F' => KeyEvent::new(modifiers, key::End),
b'H' => KeyEvent::new(modifiers, key::Home),
b'I' => KeyEvent::new(modifiers, key::Tab),
b'M' => KeyEvent::new(modifiers, key::Enter),
b' ' => KeyEvent::new(modifiers, key::SPACE),
b'A' => KeyEvent::new(modifiers, key::UP),
b'B' => KeyEvent::new(modifiers, key::DOWN),
b'C' => KeyEvent::new(modifiers, key::RIGHT),
b'D' => KeyEvent::new(modifiers, key::LEFT),
b'F' => KeyEvent::new(modifiers, key::END),
b'H' => KeyEvent::new(modifiers, key::HOME),
b'I' => KeyEvent::new(modifiers, key::TAB),
b'M' => KeyEvent::new(modifiers, key::ENTER),
b'P' => KeyEvent::new(modifiers, function_key(1)),
b'Q' => KeyEvent::new(modifiers, function_key(2)),
b'R' => KeyEvent::new(modifiers, function_key(3)),

View File

@@ -25,25 +25,25 @@ macro_rules! define_special_keys {
}
define_special_keys! {
Backspace: 0
Delete: 1
Escape: 2
Enter: 3
Up: 4
Down: 5
Left: 6
Right: 7
PageUp: 8
PageDown: 9
Home: 10
End: 11
Insert: 12
Tab: 13
Space: 14
Menu: 15
PrintScreen: 16
BACKSPACE: 0
DELETE: 1
ESCAPE: 2
ENTER: 3
UP: 4
DOWN: 5
LEFT: 6
RIGHT: 7
PAGE_UP: 8
PAGE_DOWN: 9
HOME: 10
END: 11
INSERT: 12
TAB: 13
SPACE: 14
MENU: 15
PRINT_SCREEN: 16
Invalid: 255
INVALID: 255
}
pub(crate) const MAX_FUNCTION_KEY: u8 = 12;
@@ -55,23 +55,23 @@ pub(crate) fn function_key(n: u8) -> char {
pub(crate) const KEY_NAMES: &[(char, &wstr)] = &[
('-', L!("minus")),
(',', L!("comma")),
(Backspace, L!("backspace")),
(Delete, L!("delete")),
(Escape, L!("escape")),
(Enter, L!("enter")),
(Up, L!("up")),
(Down, L!("down")),
(Left, L!("left")),
(Right, L!("right")),
(PageUp, L!("pageup")),
(PageDown, L!("pagedown")),
(Home, L!("home")),
(End, L!("end")),
(Insert, L!("insert")),
(Tab, L!("tab")),
(Space, L!("space")),
(Menu, L!("menu")),
(PrintScreen, L!("printscreen")),
(BACKSPACE, L!("backspace")),
(DELETE, L!("delete")),
(ESCAPE, L!("escape")),
(ENTER, L!("enter")),
(UP, L!("up")),
(DOWN, L!("down")),
(LEFT, L!("left")),
(RIGHT, L!("right")),
(PAGE_UP, L!("pageup")),
(PAGE_DOWN, L!("pagedown")),
(HOME, L!("home")),
(END, L!("end")),
(INSERT, L!("insert")),
(TAB, L!("tab")),
(SPACE, L!("space")),
(MENU, L!("menu")),
(PRINT_SCREEN, L!("printscreen")),
];
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
@@ -187,25 +187,25 @@ fn ascii_control(c: char) -> char {
pub(crate) fn canonicalize_keyed_control_char(c: char) -> char {
if c == ascii_control('m') {
return Enter;
return ENTER;
}
if c == ascii_control('i') {
return Tab;
return TAB;
}
if c == ' ' {
return Space;
return SPACE;
}
if let Some(tm) = get_terminal_mode_on_startup() {
if c == char::from(tm.c_cc[VERASE]) {
return Backspace;
return BACKSPACE;
}
}
if c == char::from(127) {
// when it's not backspace
return Delete;
return DELETE;
}
if c == '\x1b' {
return Escape;
return ESCAPE;
}
c
}
@@ -214,7 +214,7 @@ pub(crate) fn canonicalize_unkeyed_control_char(c: u8) -> char {
if c == 0 {
// For legacy terminals we have to make a decision here; they send NUL on Ctrl-2,
// Ctrl-Shift-2 or Ctrl-Backtick, but the most straightforward way is Ctrl-Space.
return Space;
return SPACE;
}
// Represent Ctrl-letter combinations in lower-case, to be clear
// that Shift is not involved.
@@ -350,11 +350,11 @@ pub(crate) fn canonicalize_raw_escapes(keys: Vec<Key>) -> Vec<Key> {
if had_literal_escape {
had_literal_escape = false;
if key.modifiers.alt {
canonical.push(Key::from_raw(Escape));
canonical.push(Key::from_raw(ESCAPE));
} else {
key.modifiers.alt = true;
if key.codepoint == '\x1b' {
key.codepoint = Escape;
key.codepoint = ESCAPE;
}
}
} else if key.codepoint == '\x1b' {
@@ -364,7 +364,7 @@ pub(crate) fn canonicalize_raw_escapes(keys: Vec<Key>) -> Vec<Key> {
canonical.push(key);
}
if had_literal_escape {
canonical.push(Key::from_raw(Escape));
canonical.push(Key::from_raw(ESCAPE));
}
canonical
}
@@ -482,9 +482,9 @@ mod tests {
fn test_parse_key() {
assert_eq!(
parse_keys(L!("escape")),
Ok(vec![Key::from_raw(key::Escape)])
Ok(vec![Key::from_raw(key::ESCAPE)])
);
assert_eq!(parse_keys(L!("\x1b")), Ok(vec![Key::from_raw(key::Escape)]));
assert_eq!(parse_keys(L!("\x1b")), Ok(vec![Key::from_raw(key::ESCAPE)]));
assert_eq!(parse_keys(L!("ctrl-a")), Ok(vec![ctrl('a')]));
assert_eq!(parse_keys(L!("\x01")), Ok(vec![ctrl('a')]));
assert!(parse_keys(L!("f0")).is_err());

View File

@@ -1,4 +1,4 @@
#![cfg_attr(feature = "benchmark", feature(test))]
#![cfg_attr(all(nightly, feature = "benchmark"), feature(test))]
#![allow(non_camel_case_types)]
pub const BUILD_VERSION: &str = env!("FISH_BUILD_VERSION");

View File

@@ -968,17 +968,13 @@ fn visit(&mut self, node: &'a dyn Node) {
// ....cmd3
// end
// See #7252.
Kind::JobContinuation(node) => {
if self.has_newline(&node.newlines) {
inc_dec = (1, 1);
}
Kind::JobContinuation(node) if self.has_newline(&node.newlines) => {
inc_dec = (1, 1);
}
// Likewise for && and ||.
Kind::JobConjunctionContinuation(node) => {
if self.has_newline(&node.newlines) {
inc_dec = (1, 1);
}
Kind::JobConjunctionContinuation(node) if self.has_newline(&node.newlines) => {
inc_dec = (1, 1);
}
Kind::CaseItemList(_) => {
@@ -1150,23 +1146,21 @@ pub fn detect_parse_errors_in_ast(
let mut traversal = ast::Traversal::new(ast.top());
while let Some(node) = traversal.next() {
match node.kind() {
Kind::JobContinuation(jc) => {
Kind::JobContinuation(jc)
// Somewhat clumsy way of checking for a statement without source in a pipeline.
// See if our pipe has source but our statement does not.
if jc.pipe.has_source() && jc.statement.try_source_range().is_none() {
if jc.pipe.has_source() && jc.statement.try_source_range().is_none() => {
has_unclosed_pipe = true;
}
}
Kind::JobConjunction(job_conjunction) => {
issue.error |= detect_errors_in_job_conjunction(job_conjunction, &mut out_errors);
}
Kind::JobConjunctionContinuation(jcc) => {
Kind::JobConjunctionContinuation(jcc)
// Somewhat clumsy way of checking for a job without source in a conjunction.
// See if our conjunction operator (&& or ||) has source but our job does not.
if jcc.conjunction.has_source() && jcc.job.try_source_range().is_none() {
if jcc.conjunction.has_source() && jcc.job.try_source_range().is_none() => {
has_unclosed_conjunction = true;
}
}
Kind::Argument(arg) => {
let arg_src = arg.source(buff_src);
if let Err(e) = detect_errors_in_argument(arg, arg_src, &mut out_errors) {
@@ -1174,7 +1168,7 @@ pub fn detect_parse_errors_in_ast(
issue.incomplete |= e.incomplete;
}
}
Kind::JobPipeline(job) => {
Kind::JobPipeline(job)
// Disallow background in the following cases:
//
// foo & ; and bar
@@ -1182,11 +1176,10 @@ pub fn detect_parse_errors_in_ast(
// if foo & ; end
// while foo & ; end
// If it's not a background job, nothing to do.
if job.bg.is_some() {
if job.bg.is_some() => {
issue.error |=
detect_errors_in_backgrounded_job(&traversal, job, &mut out_errors);
}
}
Kind::DecoratedStatement(stmt) => {
issue.error |= detect_errors_in_decorated_statement(
buff_src,

View File

@@ -7026,9 +7026,11 @@ fn handle_completions(&mut self, token_range: Range<usize>, mut comp: Vec<Comple
comp.retain(|c| !c.replaces_token() || reader_can_replace(&tok, c.flags));
for c in &mut comp {
if !will_replace_token && c.replaces_token() {
c.flags |= CompleteFlags::SUPPRESS_PAGER_PREFIX;
if !will_replace_token {
for c in &mut comp {
if c.replaces_token() {
c.flags |= CompleteFlags::SUPPRESS_PAGER_PREFIX;
}
}
}

View File

@@ -273,7 +273,7 @@ pub fn signal_handle(sig: Signal) {
sigaction(sig, &act, std::ptr::null_mut());
}
pub static signals_to_default: LazyLock<libc::sigset_t> = LazyLock::new(|| {
pub static SIGNALS_TO_DEFAULT: LazyLock<libc::sigset_t> = LazyLock::new(|| {
let mut set = MaybeUninit::uninit();
unsafe { libc::sigemptyset(set.as_mut_ptr()) };
for data in SIGNAL_TABLE.iter() {

View File

@@ -149,16 +149,11 @@ fn wildcard_complete_internal(
// Note: out_completion may be empty if the completion really is empty, e.g. tab-completing
// 'foo' when a file 'foo' exists.
let local_flags = if full_replacement {
flags | CompleteFlags::REPLACES_TOKEN
} else {
flags
};
if !out.add(Completion::new(
out_completion.to_owned(),
out_desc,
m,
local_flags,
flags,
)) {
return WildcardResult::Overflow;
}

View File

@@ -346,7 +346,9 @@ complete -C'cd .'
# Note that there is no kern.osproductversion under older OS X releases!
#
# NetBSD 10 does not support it.
if test (uname) = NetBSD || { test (uname) = Darwin && test (sysctl kern.osproductversion 2>/dev/null | string match -r \\d+; or echo 10) -lt 12 }
# Cygwin/MSYS does not support it when using ACL. And without ACL, a directory
# cannot be made unreadable, making the test pointless. So either way, skip it
if test (uname) = NetBSD || __fish_is_cygwin || { test (uname) = Darwin && test (sysctl kern.osproductversion 2>/dev/null | string match -r \\d+; or echo 10) -lt 12 }
# Not supported. Satisfy the CHECKs below.
echo fake/a
echo fake/a/b

View File

@@ -5,7 +5,10 @@
mkdir dir
echo "#!/bin/sh" >dir/foo.exe
echo "#!/bin/sh" >dir/foo.bar
set PATH (pwd)/dir $PATH
if ! cygwin_noacl ./
chmod u+x dir/foo*
end
set -p PATH (pwd)/dir
# === Check that `complete` prefers to non-exe name, unless the user started
# to type the extension
@@ -36,6 +39,9 @@ complete -C"./dir/foo."
# === Check that if we have a non-exe and an exe file, they both show
echo "#!/bin/sh" >dir/foo.bar.exe
if ! cygwin_noacl ./
chmod u+x dir/foo.bar.exe
end
complete -C"./dir/foo.ba"
# CHECK: ./dir/foo.bar{{\t}}command
# CHECK: ./dir/foo.bar.exe{{\t}}command

View File

@@ -727,3 +727,9 @@ complete -c foo -a "foo\\"
complete -C
# CHECKERR: complete: Can not get commandline in non-interactive mode
if string match -rq -- '^[a-z]+$' $USER
set -l first_letter_wrong_case (string sub -l 1 -- $USER | string upper)
string match -rq -- "$USER\t.*" (complete -C "echo ~$first_letter_wrong_case")
or echo "`complete -C'echo ~$first_letter_wrong_case'` did not yield $USER"
end

View File

@@ -14,6 +14,9 @@ status job-control full
sleep 1 &
set -l pid (jobs -lp)
kill -SIGSTOP $pid
while not jobs | grep -q stopped
sleep .01
end
disown
# CHECKERR: disown: job 1 ('sleep 1 &') was stopped and has been signalled to continue.
echo $status

View File

@@ -231,7 +231,14 @@ path filter -w stuff/*
echo "=== test --perm exec"
# CHECK: === test --perm exec
path filter --perm exec stuff/*
begin
path filter --perm exec stuff/*
# Cygwin with ACL doesn't return non-readable files, so add them manually to pass the test
if __fish_is_cygwin && ! set -q noacl
echo stuff/exec
echo stuff/writeexec
end
end | sort
# CHECK: stuff/all
# CHECK: stuff/exec
# CHECK: stuff/readexec
@@ -240,7 +247,13 @@ path filter --perm exec stuff/*
echo "=== test -x"
# CHECK: === test -x
path filter -x stuff/*
begin
path filter -x exec stuff/*
if __fish_is_cygwin && ! set -q noacl
echo stuff/exec
echo stuff/writeexec
end
end | sort
# CHECK: stuff/all
# CHECK: stuff/exec
# CHECK: stuff/readexec
@@ -271,7 +284,12 @@ end
echo "=== test --perm write,exec"
# CHECK: === test --perm write,exec
path filter --perm write,exec stuff/*
begin
path filter --perm write,exec stuff/*
if __fish_is_cygwin && ! set -q noacl
echo stuff/writeexec
end
end | sort
# CHECK: stuff/all
# CHECK: stuff/writeexec

View File

@@ -14,3 +14,7 @@ prompt_pwd -D 0 /usr/share/fish/prompts
prompt_pwd -d1 -D 3 /usr/local/share/fish/prompts
# CHECK: /u/l/share/fish/prompts
# Ensure control characters in paths are stripped
prompt_pwd -d 0 /foo/(printf '\e]0;OHNO\a')bar
# CHECK: /foo/]0;OHNObar

View File

@@ -1,5 +1,8 @@
#RUN: fish_indent=%fish_indent %fish %s
#REQUIRES: command -v sphinx-build
# Cygwin has some old version of python/sphinx/... triggering deprecation errors
# (and MSYS doesn't have sphinx-build at all)
#REQUIRES: %fish -c "not __fish_is_cygwin"
set -l workspace_root (status dirname)/../..
set -l build_script $workspace_root/tests/test_functions/sphinx-shared.sh

View File

@@ -0,0 +1,30 @@
#RUN: %fish %s
#REQUIRES: command -v tmux
#REQUIRES: uname -r | grep -qv Microsoft
isolated-tmux-start -C '
complete : -s c -l clip
complete : -s q -l qrcode
set -g fish_autosuggestion_enabled 0
'
touch somefile1
touch somefile2
isolated-tmux send-keys C-l ': -c'
function tab
isolated-tmux send-keys Tab
tmux-sleep
isolated-tmux capture-pane -p | awk '/./ { print "[" $0 "]" }'
end
tab
# CHECK: [prompt 0> : -cq]
tab
# CHECK: [prompt 0> : -cq somefile]
# CHECK: [somefile1 somefile2]
tab
# CHECK: [prompt 0> : -cq somefile1]
# CHECK: [somefile1 somefile2]

View File

@@ -2,10 +2,19 @@ function cygwin_nosymlinks --description \
"Return 0 if Cygwin fakes symlinks, return 1 otherwise"
switch (__fish_uname)
case "MSYS*"
not string match -q "*winsymlinks*" -- "$MSYS"
case "CYGWIN*"
not string match -q "*winsymlinks*" -- "$CYGWIN"
# Cygwin has various ways of creating symlinks but they should
# all be recognized as such (with a few exceptions, which we do
# not support in testing, see
# https://cygwin.com/cygwin-ug-net/using.html#pathnames-symlinks)
return 1
case "MSYS*"
# In addition to the standard Cygwin symlinks, MSYS2 also has
# `deepcopy` mode, which is the default, and does not create
# recognizable symlinks
# (https://www.msys2.org/docs/symlinks/)
not string match -q "*winsymlinks*" -- "$MSYS"
or string match -q "*winsymlinks:deepcopy*" -- "$MSYS"
case "*"
return 1
end

View File

@@ -6,9 +6,9 @@ sources:
spec:
shell: bash
command: |
set -eo pipefail
set -e
# Check that we have latest stable.
if rustup check | grep ^stable- | grep 'Update available'; then
if rustup check | grep '^stable-.*[uU]pdate available'; then
echo >&2 "Rust toolchain 'stable' is stale, please update it"
exit 1
fi