Skip to content

Commit 5de74d0

Browse files
authored
0.17 phase 5 PR C: defaults_keys, chown_owner_group, macos apps + spec patches (#146)
* refactor(engine): inject params["prev_arg"] for NEEDS_PREV_ARG providers * feat(defaults): defaults_keys provider for read/write/delete/rename key args * feat(chown): colon-aware chown_owner_group provider * feat(macos): macos_applications + macos_bundle_identifiers providers * feat(specs): patch open, osascript, codesign — replace requires_js with native providers/statics * chore(specs): bump unsupported-without-runtime baseline to 292; cargo fmt * chore(specs): refresh chown/codesign/defaults/open/osascript snapshots * fix: address review findings on macOS completion providers - macos_apps: parallelize bundle-id mdls lookups via bounded tokio JoinSet (was up to 500 serial subprocess spawns); split mdfind/mdls spawn errors into trace (binary missing) vs warn (timeout/crash) via is_binary_missing; emit one aggregate warn when candidates resolve zero bundle ids; extract pure app_paths_to_resolve/assemble_bundle_identifiers helpers with tests - macos_defaults: fix parse_defaults_keys desync on ';' inside quoted values; extract pure resolve_domain + add DefaultsKeys dispatch tests; promote defaults-read spawn-failure log from trace to warn - dscl_principals: correct ChownOwnerGroup doc (fetches one principal set per dispatch, not both) and ChownToken doc (engine re-ranks via fuzzy::rank) - providers/mod: add inject_prev_arg positive-path tests * fix: correct macOS provider docs, deepen defaults parser, add concurrency tests - macos_defaults: parse_defaults_keys now tracks quote state at every depth (was depth-1 only), so unbalanced braces/parens inside a nested quoted value no longer desync depth and drop sibling keys; add nested-value regression test plus end-to-end DefaultsKeys read->parse->suggestion tests (incl. -globalDomain rewrite flowing through to the real read) - dscl_principals + providers/mod: drop the false "nucleo treats ':' as a fuzzy delimiter" claim from the ChownOwnerGroup docs (nucleo matches ':' as an ordinary subsequence character), matching the corrected ChownToken doc - providers/mod: extract PREV_ARG_KEY const shared by inject_prev_arg and the DefaultsKeys consumer to prevent silent producer/consumer drift - tests: add end-to-end coverage of the bounded-concurrency mdls fan-out with a deterministic sentinel-barrier to force out-of-order JoinSet completion without racing the 1s mdls timeout under parallel-suite load * test: cover MacosApplications mapping + chown live branch; fix golden_specs doc - add MacosApplications success-path test pinning text=display-name and description=full-path (a text/description swap now fails the suite) - add ChownOwnerGroup live branch-selection test: an owner-only token fetches only /Users, a colon token fetches only /Groups - golden_specs: drop the stale "nucleo treats ':' as a delimiter" comment (nucleo matches ':' as an ordinary subsequence character), matching the corrected provider docs
1 parent f1db898 commit 5de74d0

19 files changed

Lines changed: 2055 additions & 88 deletions

crates/gc-pty/src/handler.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1897,7 +1897,14 @@ impl InputHandler {
18971897
};
18981898
let provider_engine = Arc::clone(&engine);
18991899
let provider_query = ctx.current_word.clone();
1900-
let provider_resolutions = provider_generators.clone();
1900+
let mut provider_resolutions = provider_generators.clone();
1901+
// Providers in NEEDS_PREV_ARG (e.g. defaults_keys) receive the
1902+
// previously-typed positional argument via params["prev_arg"]
1903+
// so `defaults read <domain> <key>` can dispatch with the
1904+
// resolved domain. Empty arg list is a noop.
1905+
if let Some(prev) = ctx.args.last() {
1906+
gc_suggest::providers::inject_prev_arg(&mut provider_resolutions, prev);
1907+
}
19011908
let provider_ctx_for_fut = Arc::clone(&provider_ctx);
19021909
let provider_fut = async move {
19031910
provider_engine

crates/gc-suggest/src/providers/dscl_principals.rs

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,3 +119,164 @@ impl DsclGroups {
119119
))
120120
}
121121
}
122+
123+
/// Decoded shape of a partially-typed chown owner/group token.
124+
///
125+
/// `chown` accepts `OWNER`, `OWNER:GROUP`, or `:GROUP` as a single
126+
/// argument. The provider emits pre-prefixed completions (e.g.
127+
/// `stan:staff` for the typed token `stan:`) carrying the full owner
128+
/// and colon, rather than relying on the engine to reconstruct the
129+
/// `owner:` prefix from a bare group name. At merge time the engine
130+
/// re-ranks the merged pool with `gc_suggest::fuzzy::rank` against the
131+
/// live `current_word` (the colon-containing token) — nucleo scores
132+
/// `:` as an ordinary character in a subsequence match, so a
133+
/// pre-prefixed `stan:staff` candidate still matches the query
134+
/// `stan:` and survives the re-rank.
135+
#[derive(Debug, PartialEq, Eq)]
136+
pub(crate) enum ChownToken<'a> {
137+
/// No colon typed yet. `prefix` is what's been typed so far (may
138+
/// be empty when the cursor sits at a word boundary).
139+
OwnerOnly { prefix: &'a str },
140+
/// Token starts with `:`. The user wants only a group; preserve
141+
/// the leading colon in emitted text.
142+
GroupOnly { prefix: &'a str },
143+
/// `owner:` or `owner:groupPrefix` — owner is locked in and the
144+
/// remaining completion is the group.
145+
OwnerGroup {
146+
owner: &'a str,
147+
group_prefix: &'a str,
148+
},
149+
}
150+
151+
pub(crate) fn classify_chown_token(token: &str) -> ChownToken<'_> {
152+
if let Some(rest) = token.strip_prefix(':') {
153+
return ChownToken::GroupOnly { prefix: rest };
154+
}
155+
if let Some((owner, group)) = token.split_once(':') {
156+
return ChownToken::OwnerGroup {
157+
owner,
158+
group_prefix: group,
159+
};
160+
}
161+
ChownToken::OwnerOnly { prefix: token }
162+
}
163+
164+
/// Build `chown_owner_group` suggestions from already-fetched principal
165+
/// lists. The colon-emission rules live here so they can be unit-tested
166+
/// without spawning `dscl`.
167+
///
168+
/// The `users` and `groups` slices are expected to already honour the
169+
/// caller's `include_system` decision — `parse_principals_output` is
170+
/// the right preprocessor.
171+
pub(crate) fn chown_owner_group_from_principals(
172+
token: &str,
173+
users: &[String],
174+
groups: &[String],
175+
) -> Vec<Suggestion> {
176+
match classify_chown_token(token) {
177+
ChownToken::OwnerOnly { prefix } => users
178+
.iter()
179+
.filter(|u| u.starts_with(prefix))
180+
.map(|u| Suggestion {
181+
text: u.clone(),
182+
description: Some("user".into()),
183+
kind: SuggestionKind::ProviderValue,
184+
source: SuggestionSource::Provider,
185+
..Default::default()
186+
})
187+
.collect(),
188+
ChownToken::GroupOnly { prefix } => groups
189+
.iter()
190+
.filter(|g| g.starts_with(prefix))
191+
.map(|g| Suggestion {
192+
text: format!(":{g}"),
193+
description: Some("group".into()),
194+
kind: SuggestionKind::ProviderValue,
195+
source: SuggestionSource::Provider,
196+
..Default::default()
197+
})
198+
.collect(),
199+
ChownToken::OwnerGroup {
200+
owner,
201+
group_prefix,
202+
} => groups
203+
.iter()
204+
.filter(|g| g.starts_with(group_prefix))
205+
.map(|g| Suggestion {
206+
text: format!("{owner}:{g}"),
207+
description: Some(format!("user {owner}, group {g}")),
208+
kind: SuggestionKind::ProviderValue,
209+
source: SuggestionSource::Provider,
210+
..Default::default()
211+
})
212+
.collect(),
213+
}
214+
}
215+
216+
/// `chown_owner_group` — colon-aware completion for chown's first
217+
/// positional argument. Replaces the legacy `[dscl_users, dscl_groups]`
218+
/// pair, which couldn't account for the `OWNER:GROUP` form because the
219+
/// colon must be carried into the emitted candidate (e.g. `stan:staff`)
220+
/// rather than reconstructed by the engine from a bare group name —
221+
/// nucleo matches `:` as an ordinary subsequence character, so a
222+
/// pre-prefixed candidate still matches a `stan:` query (see the
223+
/// [`ChownToken`] doc above).
224+
///
225+
/// The dispatch fetches ONLY the principal set the token shape needs —
226+
/// `/Users` for an owner-only token, `/Groups` for a `:group` or
227+
/// `owner:group` token — via mutually exclusive branches, so each
228+
/// completion spawns at most one `dscl` call (never both). The pure
229+
/// [`chown_owner_group_from_principals`] then formats the surfaced set
230+
/// based on the same token shape.
231+
pub struct ChownOwnerGroup;
232+
233+
impl Provider for ChownOwnerGroup {
234+
fn name(&self) -> &'static str {
235+
"chown_owner_group"
236+
}
237+
238+
async fn generate(&self, ctx: &ProviderCtx) -> Result<Vec<Suggestion>> {
239+
self.generate_with_binary(ctx, "dscl").await
240+
}
241+
}
242+
243+
impl ChownOwnerGroup {
244+
pub(crate) async fn generate_with_binary(
245+
&self,
246+
ctx: &ProviderCtx,
247+
binary: &str,
248+
) -> Result<Vec<Suggestion>> {
249+
let include_system = include_system_from_ctx(ctx);
250+
let token = ctx.current_token.as_str();
251+
let want_groups = matches!(
252+
classify_chown_token(token),
253+
ChownToken::GroupOnly { .. } | ChownToken::OwnerGroup { .. }
254+
);
255+
256+
let users = if matches!(classify_chown_token(token), ChownToken::OwnerOnly { .. }) {
257+
match run_dscl_list_with_binary(&ctx.cwd, binary, "/Users").await {
258+
Some(output) => parse_principals_output(&output, include_system, "user")
259+
.into_iter()
260+
.map(|s| s.text)
261+
.collect::<Vec<_>>(),
262+
None => Vec::new(),
263+
}
264+
} else {
265+
Vec::new()
266+
};
267+
268+
let groups = if want_groups {
269+
match run_dscl_list_with_binary(&ctx.cwd, binary, "/Groups").await {
270+
Some(output) => parse_principals_output(&output, include_system, "group")
271+
.into_iter()
272+
.map(|s| s.text)
273+
.collect::<Vec<_>>(),
274+
None => Vec::new(),
275+
}
276+
} else {
277+
Vec::new()
278+
};
279+
280+
Ok(chown_owner_group_from_principals(token, &users, &groups))
281+
}
282+
}

0 commit comments

Comments
 (0)