1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
//! Port of `_history` from `Completion/Base/Completer/_history`.
//!
//! Full upstream body (65 lines, abridged):
//! ```text
//! sh: 1 #autoload
//! sh:19 local opt expl max slice hmax=$#historywords beg=2
//! sh:21 if zstyle -t … remove-all-dups; then opt=- else opt=-1 fi
//! sh:27 if zstyle -t … sort; then opt+=J else opt+=V fi
//! sh:33 if zstyle -s … range max; then …
//! sh:46 PREFIX="$IPREFIX$PREFIX"
//! sh:48 SUFFIX="$SUFFIX$ISUFFIX"
//! sh:55 while [[ $compstate[nmatches] -eq 0 && beg -lt max ]]; do
//! sh:56 if [[ -n $compstate[quote] ]]
//! sh:57 then hslice=( ${(Q)historywords[beg,beg+slice]} )
//! sh:58 else hslice=( ${historywords[beg,beg+slice]} )
//! sh:59 fi
//! sh:60 _wanted "$opt" history-words expl 'history word' \
//! sh:61 compadd -Q -a hslice
//! sh:62 (( beg+=slice ))
//! sh:63 done
//! sh:65 (( $compstate[nmatches] ))
//! ```
use crate::compsys::ported::_wanted::_wanted;
use crate::compsys::ported::shared::zstyle_t;
use crate::ported::modules::zutil::lookupstyle;
use crate::ported::params::{getaparam, getsparam, setaparam, setsparam};
use crate::ported::zle::compcore::get_compstate_str;
/// `_history` — complete from `$historywords` array.
pub fn _history() -> i32 {
let _fn_scope = crate::compsys::ported::shared::FnScope::enter("_history");
// sh:19 — `local opt expl max slice hmax=$#historywords beg=2`.
//
// This port does not assign `expl` itself; it hands the NAME to
// `_wanted`/`_description`, and `_description` fills it through
// `setaparam` — `createparam(name, PM_SCALAR)` with no PM_LOCAL
// (shared.rs:16-30). The array was therefore born at level 0 and
// `endparamscope` had nothing to unwind, so one TAB left `expl`
// in the user's shell. Measured through a pty, `${(t)expl}` and
// the value read back at the next prompt after a single TAB on a
// `_history` wrapper:
//
// zsh : [][]
// zshrs: [array][-J|-default-]
//
// kind 0, as sh:19 spells a bare `local`.
crate::compsys::ported::shared::declare_locals(&["expl"], 0);
// sh:54 — `local -a hslice`.
//
// `hslice` is the window of `$historywords` this loop hands to
// `_wanted` by name (sh:60-63). The port writes it as a shell
// parameter, so without the declaration a completion that fell through
// to `_history` left the slice standing in the user's shell:
//
// zsh : hslice=[][0] zshrs: hslice=[array][3]
//
// `historywords`, also written here, is deliberately NOT declared: it
// is the zsh/parameter special the completion widget publishes, not a
// scratch name of this function.
crate::compsys::ported::shared::declare_locals(
&["hslice"],
crate::compsys::ported::shared::PM_ARRAY,
);
let historywords = getaparam("historywords").unwrap_or_default();
let hmax = historywords.len();
let mut beg = 2usize;
let curcontext = getsparam("curcontext").unwrap_or_default();
let ctx = format!(":completion:{}:", curcontext);
// sh:21 — `zstyle -t`, a VALUE test; see [`zstyle_t`].
let remove_all_dups = zstyle_t(&ctx, "remove-all-dups") == 0;
let opt_prefix = if remove_all_dups { "-" } else { "-1" };
// sh:27 — `zstyle -t`, a VALUE test; see [`zstyle_t`].
let sort_on = zstyle_t(&ctx, "sort") == 0;
let opt = if sort_on {
format!("{}J", opt_prefix)
} else {
format!("{}V", opt_prefix)
};
// sh:33-40 range style
let range_val = lookupstyle(&ctx, "range")
.first()
.cloned()
.unwrap_or_default();
let (mut max, slice): (usize, usize) = if !range_val.is_empty() {
let (m_str, s_str) = if range_val.contains(':') {
let mut parts = range_val.splitn(2, ':');
let m = parts.next().unwrap_or("0").to_string();
let s = parts.next().unwrap_or("0").to_string();
(m, s)
} else {
(range_val.clone(), range_val)
};
let m = m_str.parse::<usize>().unwrap_or(hmax);
let s = s_str.parse::<usize>().unwrap_or(m);
(m.min(hmax), s)
} else {
(hmax, hmax)
};
if max > hmax {
max = hmax;
}
// sh:42-46 flatten quoted/unquoted PREFIX/IPREFIX semantics
let prefix = getsparam("PREFIX").unwrap_or_default();
let iprefix = getsparam("IPREFIX").unwrap_or_default();
let _ = setsparam("PREFIX", &format!("{}{}", iprefix, prefix));
let _ = setsparam("IPREFIX", "");
let suffix = getsparam("SUFFIX").unwrap_or_default();
let isuffix = getsparam("ISUFFIX").unwrap_or_default();
let _ = setsparam("SUFFIX", &format!("{}{}", suffix, isuffix));
let _ = setsparam("ISUFFIX", "");
// sh:50-59 walk slices until nmatches > 0 or beg >= max.
while get_compstate_str("nmatches").and_then(|s| s.parse::<i64>().ok()) == Some(0) && beg < max
{
let end = (beg + slice).min(hmax);
let hslice: Vec<String> = if beg <= end && beg >= 1 && end <= historywords.len() {
historywords[beg - 1..end].to_vec()
} else {
Vec::new()
};
setaparam("hslice", hslice);
let _ = _wanted(&[
opt.clone(),
"history-words".to_string(),
"expl".to_string(),
"history word".to_string(),
"compadd".to_string(),
"-Q".to_string(),
"-a".to_string(),
"hslice".to_string(),
]);
beg += slice;
if slice == 0 {
break;
}
}
// sh:65
let nm: i64 = get_compstate_str("nmatches")
.and_then(|s| s.parse().ok())
.unwrap_or(0);
if nm == 0 {
1
} else {
0
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_historywords_returns_one() {
let _g = crate::test_util::global_state_lock();
// `compstate[nmatches]` is a LIVE GSU integer (complete.c:1411)
// backed by the `nmatches` counter — writing it through
// `set_compstate_str` is a no-op, the reader always consults the
// counter. Zero the counter itself so a non-zero count left by
// an earlier completion test doesn't turn "no matches" into 0.
crate::ported::zle::compcore::nmatches.store(0, std::sync::atomic::Ordering::Relaxed);
setaparam("historywords", Vec::new());
assert_eq!(_history(), 1);
}
}