just playing with tangled
at main 1240 lines 44 kB view raw
1// Copyright 2024 The Jujutsu Authors 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// https://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15use std::io::BufRead as _; 16 17use clap::builder::StyledStr; 18use clap::FromArgMatches as _; 19use clap_complete::CompletionCandidate; 20use itertools::Itertools as _; 21use jj_lib::config::ConfigNamePathBuf; 22use jj_lib::settings::UserSettings; 23use jj_lib::workspace::DefaultWorkspaceLoaderFactory; 24use jj_lib::workspace::WorkspaceLoaderFactory as _; 25 26use crate::cli_util::expand_args; 27use crate::cli_util::find_workspace_dir; 28use crate::cli_util::load_template_aliases; 29use crate::cli_util::GlobalArgs; 30use crate::command_error::user_error; 31use crate::command_error::CommandError; 32use crate::config::config_from_environment; 33use crate::config::default_config_layers; 34use crate::config::ConfigArgKind; 35use crate::config::ConfigEnv; 36use crate::config::CONFIG_SCHEMA; 37use crate::revset_util::load_revset_aliases; 38use crate::ui::Ui; 39 40const BOOKMARK_HELP_TEMPLATE: &str = r#"template-aliases.'bookmark_help()'=''' 41" " ++ 42if(normal_target, 43 if(normal_target.description(), 44 normal_target.description().first_line(), 45 "(no description set)", 46 ), 47 "(conflicted bookmark)", 48) 49'''"#; 50 51/// A helper function for various completer functions. It returns 52/// (candidate, help) assuming they are separated by a space. 53fn split_help_text(line: &str) -> (&str, Option<StyledStr>) { 54 match line.split_once(' ') { 55 Some((name, help)) => (name, Some(help.to_string().into())), 56 None => (line, None), 57 } 58} 59 60pub fn local_bookmarks() -> Vec<CompletionCandidate> { 61 with_jj(|jj, _| { 62 let output = jj 63 .build() 64 .arg("bookmark") 65 .arg("list") 66 .arg("--config") 67 .arg(BOOKMARK_HELP_TEMPLATE) 68 .arg("--template") 69 .arg(r#"if(!remote, name ++ bookmark_help()) ++ "\n""#) 70 .output() 71 .map_err(user_error)?; 72 73 Ok(String::from_utf8_lossy(&output.stdout) 74 .lines() 75 .map(split_help_text) 76 .map(|(name, help)| CompletionCandidate::new(name).help(help)) 77 .collect()) 78 }) 79} 80 81pub fn tracked_bookmarks() -> Vec<CompletionCandidate> { 82 with_jj(|jj, _| { 83 let output = jj 84 .build() 85 .arg("bookmark") 86 .arg("list") 87 .arg("--tracked") 88 .arg("--config") 89 .arg(BOOKMARK_HELP_TEMPLATE) 90 .arg("--template") 91 .arg(r#"if(remote, name ++ '@' ++ remote ++ bookmark_help() ++ "\n")"#) 92 .output() 93 .map_err(user_error)?; 94 95 Ok(String::from_utf8_lossy(&output.stdout) 96 .lines() 97 .map(split_help_text) 98 .map(|(name, help)| CompletionCandidate::new(name).help(help)) 99 .collect()) 100 }) 101} 102 103pub fn untracked_bookmarks() -> Vec<CompletionCandidate> { 104 with_jj(|jj, settings| { 105 let output = jj 106 .build() 107 .arg("bookmark") 108 .arg("list") 109 .arg("--all-remotes") 110 .arg("--config") 111 .arg(BOOKMARK_HELP_TEMPLATE) 112 .arg("--template") 113 .arg( 114 r#"if(remote && !tracked && remote != "git", 115 name ++ '@' ++ remote ++ bookmark_help() ++ "\n" 116 )"#, 117 ) 118 .output() 119 .map_err(user_error)?; 120 121 let prefix = settings.get_string("git.push-bookmark-prefix").ok(); 122 123 Ok(String::from_utf8_lossy(&output.stdout) 124 .lines() 125 .map(|line| { 126 let (name, help) = split_help_text(line); 127 128 let display_order = match prefix.as_ref() { 129 // own bookmarks are more interesting 130 Some(prefix) if name.starts_with(prefix) => 0, 131 _ => 1, 132 }; 133 CompletionCandidate::new(name) 134 .help(help) 135 .display_order(Some(display_order)) 136 }) 137 .collect()) 138 }) 139} 140 141pub fn bookmarks() -> Vec<CompletionCandidate> { 142 with_jj(|jj, settings| { 143 let output = jj 144 .build() 145 .arg("bookmark") 146 .arg("list") 147 .arg("--all-remotes") 148 .arg("--config") 149 .arg(BOOKMARK_HELP_TEMPLATE) 150 .arg("--template") 151 .arg( 152 // only provide help for local refs, remote could be ambiguous 153 r#"name ++ if(remote, "@" ++ remote, bookmark_help()) ++ "\n""#, 154 ) 155 .output() 156 .map_err(user_error)?; 157 let stdout = String::from_utf8_lossy(&output.stdout); 158 159 let prefix = settings.get_string("git.push-bookmark-prefix").ok(); 160 161 Ok((&stdout 162 .lines() 163 .map(split_help_text) 164 .chunk_by(|(name, _)| name.split_once('@').map(|t| t.0).unwrap_or(name))) 165 .into_iter() 166 .map(|(bookmark, mut refs)| { 167 let help = refs.find_map(|(_, help)| help); 168 169 let local = help.is_some(); 170 let mine = prefix.as_ref().is_some_and(|p| bookmark.starts_with(p)); 171 172 let display_order = match (local, mine) { 173 (true, true) => 0, 174 (true, false) => 1, 175 (false, true) => 2, 176 (false, false) => 3, 177 }; 178 CompletionCandidate::new(bookmark) 179 .help(help) 180 .display_order(Some(display_order)) 181 }) 182 .collect()) 183 }) 184} 185 186pub fn git_remotes() -> Vec<CompletionCandidate> { 187 with_jj(|jj, _| { 188 let output = jj 189 .build() 190 .arg("git") 191 .arg("remote") 192 .arg("list") 193 .output() 194 .map_err(user_error)?; 195 196 let stdout = String::from_utf8_lossy(&output.stdout); 197 198 Ok(stdout 199 .lines() 200 .filter_map(|line| line.split_once(' ').map(|(name, _url)| name)) 201 .map(CompletionCandidate::new) 202 .collect()) 203 }) 204} 205 206pub fn template_aliases() -> Vec<CompletionCandidate> { 207 with_jj(|_, settings| { 208 let Ok(template_aliases) = load_template_aliases(&Ui::null(), settings.config()) else { 209 return Ok(Vec::new()); 210 }; 211 Ok(template_aliases 212 .symbol_names() 213 .map(CompletionCandidate::new) 214 .sorted() 215 .collect()) 216 }) 217} 218 219pub fn aliases() -> Vec<CompletionCandidate> { 220 with_jj(|_, settings| { 221 Ok(settings 222 .table_keys("aliases") 223 // This is opinionated, but many people probably have several 224 // single- or two-letter aliases they use all the time. These 225 // aliases don't need to be completed and they would only clutter 226 // the output of `jj <TAB>`. 227 .filter(|alias| alias.len() > 2) 228 .map(CompletionCandidate::new) 229 .collect()) 230 }) 231} 232 233fn revisions(match_prefix: &str, revset_filter: Option<&str>) -> Vec<CompletionCandidate> { 234 with_jj(|jj, settings| { 235 // display order 236 const LOCAL_BOOKMARK_MINE: usize = 0; 237 const LOCAL_BOOKMARK: usize = 1; 238 const TAG: usize = 2; 239 const CHANGE_ID: usize = 3; 240 const REMOTE_BOOKMARK_MINE: usize = 4; 241 const REMOTE_BOOKMARK: usize = 5; 242 const REVSET_ALIAS: usize = 6; 243 244 let mut candidates = Vec::new(); 245 246 // bookmarks 247 248 let prefix = settings.get_string("git.push-bookmark-prefix").ok(); 249 250 let mut cmd = jj.build(); 251 cmd.arg("bookmark") 252 .arg("list") 253 .arg("--all-remotes") 254 .arg("--config") 255 .arg(BOOKMARK_HELP_TEMPLATE) 256 .arg("--template") 257 .arg( 258 r#"if(remote != "git", name ++ if(remote, "@" ++ remote) ++ bookmark_help() ++ "\n")"#, 259 ); 260 if let Some(revs) = revset_filter { 261 cmd.arg("--revisions").arg(revs); 262 } 263 let output = cmd.output().map_err(user_error)?; 264 let stdout = String::from_utf8_lossy(&output.stdout); 265 266 candidates.extend( 267 stdout 268 .lines() 269 .map(split_help_text) 270 .filter(|(bookmark, _)| bookmark.starts_with(match_prefix)) 271 .map(|(bookmark, help)| { 272 let local = !bookmark.contains('@'); 273 let mine = prefix.as_ref().is_some_and(|p| bookmark.starts_with(p)); 274 275 let display_order = match (local, mine) { 276 (true, true) => LOCAL_BOOKMARK_MINE, 277 (true, false) => LOCAL_BOOKMARK, 278 (false, true) => REMOTE_BOOKMARK_MINE, 279 (false, false) => REMOTE_BOOKMARK, 280 }; 281 CompletionCandidate::new(bookmark) 282 .help(help) 283 .display_order(Some(display_order)) 284 }), 285 ); 286 287 // tags 288 289 // Tags cannot be filtered by revisions. In order to avoid suggesting 290 // immutable tags for mutable revision args, we skip tags entirely if 291 // revset_filter is set. This is not a big loss, since tags usually point 292 // to immutable revisions anyway. 293 if revset_filter.is_none() { 294 let output = jj 295 .build() 296 .arg("tag") 297 .arg("list") 298 .arg("--config") 299 .arg(BOOKMARK_HELP_TEMPLATE) 300 .arg("--template") 301 .arg(r#"name ++ bookmark_help() ++ "\n""#) 302 .arg(format!("glob:{}*", glob::Pattern::escape(match_prefix))) 303 .output() 304 .map_err(user_error)?; 305 let stdout = String::from_utf8_lossy(&output.stdout); 306 307 candidates.extend(stdout.lines().map(|line| { 308 let (name, desc) = split_help_text(line); 309 CompletionCandidate::new(name) 310 .help(desc) 311 .display_order(Some(TAG)) 312 })); 313 } 314 315 // change IDs 316 317 let revisions = revset_filter 318 .map(String::from) 319 .or_else(|| settings.get_string("revsets.short-prefixes").ok()) 320 .or_else(|| settings.get_string("revsets.log").ok()) 321 .unwrap_or_default(); 322 323 let output = jj 324 .build() 325 .arg("log") 326 .arg("--no-graph") 327 .arg("--limit") 328 .arg("100") 329 .arg("--revisions") 330 .arg(revisions) 331 .arg("--template") 332 .arg(r#"change_id.shortest() ++ " " ++ if(description, description.first_line(), "(no description set)") ++ "\n""#) 333 .output() 334 .map_err(user_error)?; 335 let stdout = String::from_utf8_lossy(&output.stdout); 336 337 candidates.extend( 338 stdout 339 .lines() 340 .map(split_help_text) 341 .filter(|(id, _)| id.starts_with(match_prefix)) 342 .map(|(id, desc)| { 343 CompletionCandidate::new(id) 344 .help(desc) 345 .display_order(Some(CHANGE_ID)) 346 }), 347 ); 348 349 // revset aliases 350 351 let revset_aliases = load_revset_aliases(&Ui::null(), settings.config())?; 352 let mut symbol_names: Vec<_> = revset_aliases.symbol_names().collect(); 353 symbol_names.sort(); 354 candidates.extend( 355 symbol_names 356 .into_iter() 357 .filter(|symbol| symbol.starts_with(match_prefix)) 358 .map(|symbol| { 359 let (_, defn) = revset_aliases.get_symbol(symbol).unwrap(); 360 CompletionCandidate::new(symbol) 361 .help(Some(defn.into())) 362 .display_order(Some(REVSET_ALIAS)) 363 }), 364 ); 365 366 Ok(candidates) 367 }) 368} 369 370fn revset_expression( 371 current: &std::ffi::OsStr, 372 revset_filter: Option<&str>, 373) -> Vec<CompletionCandidate> { 374 let Some(current) = current.to_str() else { 375 return Vec::new(); 376 }; 377 let (prepend, match_prefix) = split_revset_trailing_name(current).unwrap_or(("", current)); 378 let candidates = revisions(match_prefix, revset_filter); 379 if prepend.is_empty() { 380 candidates 381 } else { 382 candidates 383 .into_iter() 384 .map(|candidate| candidate.add_prefix(prepend)) 385 .collect() 386 } 387} 388 389pub fn revset_expression_all(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> { 390 revset_expression(current, None) 391} 392 393pub fn revset_expression_mutable(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> { 394 revset_expression(current, Some("mutable()")) 395} 396 397/// Identifies if an incomplete expression ends with a name, or may be continued 398/// with a name. 399/// 400/// If the expression ends with an name or a partial name, returns a tuple that 401/// splits the string at the point the name starts. 402/// If the expression is empty or ends with a prefix or infix operator that 403/// could plausibly be followed by a name, returns a tuple where the first 404/// item is the entire input string, and the second item is empty. 405/// Otherwise, returns `None`. 406/// 407/// The input expression may be incomplete (e.g. missing closing parentheses), 408/// and the ability to reject invalid expressions is limited. 409fn split_revset_trailing_name(incomplete_revset_str: &str) -> Option<(&str, &str)> { 410 let final_part = incomplete_revset_str 411 .rsplit_once([':', '~', '|', '&', '(', ',']) 412 .map(|(_, rest)| rest) 413 .unwrap_or(incomplete_revset_str); 414 let final_part = final_part 415 .rsplit_once("..") 416 .map(|(_, rest)| rest) 417 .unwrap_or(final_part) 418 .trim_ascii_start(); 419 420 let re = regex::Regex::new(r"^(?:[\p{XID_CONTINUE}_/]+[@.+-])*[\p{XID_CONTINUE}_/]*$").unwrap(); 421 re.is_match(final_part) 422 .then(|| incomplete_revset_str.split_at(incomplete_revset_str.len() - final_part.len())) 423} 424 425pub fn operations() -> Vec<CompletionCandidate> { 426 with_jj(|jj, _| { 427 let output = jj 428 .build() 429 .arg("operation") 430 .arg("log") 431 .arg("--no-graph") 432 .arg("--limit") 433 .arg("100") 434 .arg("--template") 435 .arg( 436 r#" 437 separate(" ", 438 id.short(), 439 "(" ++ format_timestamp(time.end()) ++ ")", 440 description.first_line(), 441 ) ++ "\n""#, 442 ) 443 .output() 444 .map_err(user_error)?; 445 446 Ok(String::from_utf8_lossy(&output.stdout) 447 .lines() 448 .map(|line| { 449 let (id, help) = split_help_text(line); 450 CompletionCandidate::new(id).help(help) 451 }) 452 .collect()) 453 }) 454} 455 456pub fn workspaces() -> Vec<CompletionCandidate> { 457 with_jj(|jj, _| { 458 let output = jj 459 .build() 460 .arg("--config") 461 .arg(r#"templates.commit_summary='if(description, description.first_line(), "(no description set)")'"#) 462 .arg("workspace") 463 .arg("list") 464 .output() 465 .map_err(user_error)?; 466 let stdout = String::from_utf8_lossy(&output.stdout); 467 468 Ok(stdout 469 .lines() 470 .map(|line| { 471 let (name, desc) = line.split_once(": ").unwrap_or((line, "")); 472 CompletionCandidate::new(name).help(Some(desc.to_string().into())) 473 }) 474 .collect()) 475 }) 476} 477 478fn config_keys_rec( 479 prefix: ConfigNamePathBuf, 480 properties: &serde_json::Map<String, serde_json::Value>, 481 acc: &mut Vec<CompletionCandidate>, 482 only_leaves: bool, 483 suffix: &str, 484) { 485 for (key, value) in properties { 486 let mut prefix = prefix.clone(); 487 prefix.push(key); 488 489 let value = value.as_object().unwrap(); 490 match value.get("type").and_then(|v| v.as_str()) { 491 Some("object") => { 492 if !only_leaves { 493 let help = value 494 .get("description") 495 .map(|desc| desc.as_str().unwrap().to_string().into()); 496 let escaped_key = prefix.to_string(); 497 acc.push(CompletionCandidate::new(escaped_key).help(help)); 498 } 499 let Some(properties) = value.get("properties") else { 500 continue; 501 }; 502 let properties = properties.as_object().unwrap(); 503 config_keys_rec(prefix, properties, acc, only_leaves, suffix); 504 } 505 _ => { 506 let help = value 507 .get("description") 508 .map(|desc| desc.as_str().unwrap().to_string().into()); 509 let escaped_key = format!("{prefix}{suffix}"); 510 acc.push(CompletionCandidate::new(escaped_key).help(help)); 511 } 512 } 513 } 514} 515 516fn json_keypath<'a>( 517 schema: &'a serde_json::Value, 518 keypath: &str, 519 separator: &str, 520) -> Option<&'a serde_json::Value> { 521 keypath 522 .split(separator) 523 .try_fold(schema, |value, step| value.get(step)) 524} 525fn jsonschema_keypath<'a>( 526 schema: &'a serde_json::Value, 527 keypath: &ConfigNamePathBuf, 528) -> Option<&'a serde_json::Value> { 529 keypath.components().try_fold(schema, |value, step| { 530 let value = value.as_object()?; 531 if value.get("type")?.as_str()? != "object" { 532 return None; 533 } 534 let properties = value.get("properties")?.as_object()?; 535 properties.get(step.get()) 536 }) 537} 538 539fn config_values(path: &ConfigNamePathBuf) -> Option<Vec<String>> { 540 let schema: serde_json::Value = serde_json::from_str(CONFIG_SCHEMA).unwrap(); 541 542 let mut config_entry = jsonschema_keypath(&schema, path)?; 543 if let Some(reference) = config_entry.get("$ref") { 544 let reference = reference.as_str()?.strip_prefix("#/")?; 545 config_entry = json_keypath(&schema, reference, "/")?; 546 }; 547 548 if let Some(possible_values) = config_entry.get("enum") { 549 return Some( 550 possible_values 551 .as_array()? 552 .iter() 553 .filter_map(|val| val.as_str()) 554 .map(ToOwned::to_owned) 555 .collect(), 556 ); 557 } 558 559 Some(match config_entry.get("type")?.as_str()? { 560 "boolean" => vec!["false".into(), "true".into()], 561 _ => vec![], 562 }) 563} 564 565fn config_keys_impl(only_leaves: bool, suffix: &str) -> Vec<CompletionCandidate> { 566 let schema: serde_json::Value = serde_json::from_str(CONFIG_SCHEMA).unwrap(); 567 let schema = schema.as_object().unwrap(); 568 let properties = schema["properties"].as_object().unwrap(); 569 570 let mut candidates = Vec::new(); 571 config_keys_rec( 572 ConfigNamePathBuf::root(), 573 properties, 574 &mut candidates, 575 only_leaves, 576 suffix, 577 ); 578 candidates 579} 580 581pub fn config_keys() -> Vec<CompletionCandidate> { 582 config_keys_impl(false, "") 583} 584 585pub fn leaf_config_keys() -> Vec<CompletionCandidate> { 586 config_keys_impl(true, "") 587} 588 589pub fn leaf_config_key_value(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> { 590 let Some(current) = current.to_str() else { 591 return Vec::new(); 592 }; 593 594 if let Some((key, current_val)) = current.split_once('=') { 595 let Ok(key) = key.parse() else { 596 return Vec::new(); 597 }; 598 let possible_values = config_values(&key).unwrap_or_default(); 599 600 possible_values 601 .into_iter() 602 .filter(|x| x.starts_with(current_val)) 603 .map(|x| CompletionCandidate::new(format!("{key}={x}"))) 604 .collect() 605 } else { 606 config_keys_impl(true, "=") 607 .into_iter() 608 .filter(|candidate| candidate.get_value().to_str().unwrap().starts_with(current)) 609 .collect() 610 } 611} 612 613pub fn branch_name_equals_any_revision(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> { 614 let Some(current) = current.to_str() else { 615 return Vec::new(); 616 }; 617 618 let Some((branch_name, revision)) = current.split_once('=') else { 619 // Don't complete branch names since we want to create a new branch 620 return Vec::new(); 621 }; 622 revset_expression(revision.as_ref(), None) 623 .into_iter() 624 .map(|rev| rev.add_prefix(format!("{branch_name}="))) 625 .collect() 626} 627 628fn dir_prefix_from<'a>(path: &'a str, current: &str) -> Option<&'a str> { 629 path[current.len()..] 630 .split_once(std::path::MAIN_SEPARATOR) 631 .map(|(next, _)| path.split_at(current.len() + next.len() + 1).0) 632} 633 634fn current_prefix_to_fileset(current: &str) -> String { 635 let cur_esc = glob::Pattern::escape(current); 636 let dir_pat = format!("{cur_esc}*/**"); 637 let path_pat = format!("{cur_esc}*"); 638 format!("glob:{dir_pat:?} | glob:{path_pat:?}") 639} 640 641fn all_files_from_rev(rev: String, current: &std::ffi::OsStr) -> Vec<CompletionCandidate> { 642 let Some(current) = current.to_str() else { 643 return Vec::new(); 644 }; 645 with_jj(|jj, _| { 646 let mut child = jj 647 .build() 648 .arg("file") 649 .arg("list") 650 .arg("--revision") 651 .arg(rev) 652 .arg(current_prefix_to_fileset(current)) 653 .stdout(std::process::Stdio::piped()) 654 .stderr(std::process::Stdio::null()) 655 .spawn() 656 .map_err(user_error)?; 657 let stdout = child.stdout.take().unwrap(); 658 659 Ok(std::io::BufReader::new(stdout) 660 .lines() 661 .take(1_000) 662 .map_while(Result::ok) 663 .map(|path| { 664 if let Some(dir_path) = dir_prefix_from(&path, current) { 665 return CompletionCandidate::new(dir_path); 666 } 667 CompletionCandidate::new(path) 668 }) 669 .dedup() // directories may occur multiple times 670 .collect()) 671 }) 672} 673 674fn modified_files_from_rev_with_jj_cmd( 675 rev: (String, Option<String>), 676 mut cmd: std::process::Command, 677 current: &std::ffi::OsStr, 678) -> Result<Vec<CompletionCandidate>, CommandError> { 679 let Some(current) = current.to_str() else { 680 return Ok(Vec::new()); 681 }; 682 cmd.arg("diff") 683 .arg("--summary") 684 .arg(current_prefix_to_fileset(current)); 685 match rev { 686 (rev, None) => cmd.arg("--revisions").arg(rev), 687 (from, Some(to)) => cmd.arg("--from").arg(from).arg("--to").arg(to), 688 }; 689 let output = cmd.output().map_err(user_error)?; 690 let stdout = String::from_utf8_lossy(&output.stdout); 691 692 let mut candidates = Vec::new(); 693 // store renamed paths in a separate vector so we don't have to sort later 694 let mut renamed = Vec::new(); 695 696 'line_loop: for line in stdout.lines() { 697 let (mode, path) = line 698 .split_once(' ') 699 .expect("diff --summary should contain a space between mode and path"); 700 701 fn path_to_candidate(current: &str, mode: &str, p: impl AsRef<str>) -> CompletionCandidate { 702 let p = p.as_ref(); 703 if let Some(dir_path) = dir_prefix_from(p, current) { 704 return CompletionCandidate::new(dir_path); 705 } 706 707 let help = match mode { 708 "M" => "Modified".into(), 709 "D" => "Deleted".into(), 710 "A" => "Added".into(), 711 "R" => "Renamed".into(), 712 "C" => "Copied".into(), 713 _ => format!("unknown mode: '{mode}'"), 714 }; 715 CompletionCandidate::new(p).help(Some(help.into())) 716 } 717 718 // In case of a rename, one line of `diff --summary` results in 719 // two suggestions. 720 if mode == "R" { 721 'split_renamed_paths: { 722 let Some((prefix, rest)) = path.split_once('{') else { 723 break 'split_renamed_paths; 724 }; 725 let Some((rename, suffix)) = rest.split_once('}') else { 726 break 'split_renamed_paths; 727 }; 728 let Some((before, after)) = rename.split_once(" => ") else { 729 break 'split_renamed_paths; 730 }; 731 let before = format!("{prefix}{before}{suffix}"); 732 let after = format!("{prefix}{after}{suffix}"); 733 candidates.push(path_to_candidate(current, mode, before)); 734 renamed.push(path_to_candidate(current, mode, after)); 735 continue 'line_loop; 736 }; 737 } 738 739 candidates.push(path_to_candidate(current, mode, path)); 740 } 741 candidates.extend(renamed); 742 candidates.dedup(); 743 744 Ok(candidates) 745} 746 747fn modified_files_from_rev( 748 rev: (String, Option<String>), 749 current: &std::ffi::OsStr, 750) -> Vec<CompletionCandidate> { 751 with_jj(|jj, _| modified_files_from_rev_with_jj_cmd(rev, jj.build(), current)) 752} 753 754fn conflicted_files_from_rev(rev: &str, current: &std::ffi::OsStr) -> Vec<CompletionCandidate> { 755 let Some(current) = current.to_str() else { 756 return Vec::new(); 757 }; 758 with_jj(|jj, _| { 759 let output = jj 760 .build() 761 .arg("resolve") 762 .arg("--list") 763 .arg("--revision") 764 .arg(rev) 765 .arg(current_prefix_to_fileset(current)) 766 .output() 767 .map_err(user_error)?; 768 let stdout = String::from_utf8_lossy(&output.stdout); 769 770 Ok(stdout 771 .lines() 772 .map(|line| { 773 let path = line 774 .split_whitespace() 775 .next() 776 .expect("resolve --list should contain whitespace after path"); 777 778 if let Some(dir_path) = dir_prefix_from(path, current) { 779 return CompletionCandidate::new(dir_path); 780 } 781 CompletionCandidate::new(path) 782 }) 783 .dedup() // directories may occur multiple times 784 .collect()) 785 }) 786} 787 788pub fn modified_files(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> { 789 modified_files_from_rev(("@".into(), None), current) 790} 791 792pub fn all_revision_files(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> { 793 all_files_from_rev(parse::revision_or_wc(), current) 794} 795 796pub fn modified_revision_files(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> { 797 modified_files_from_rev((parse::revision_or_wc(), None), current) 798} 799 800pub fn modified_range_files(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> { 801 match parse::range() { 802 Some((from, to)) => modified_files_from_rev((from, Some(to)), current), 803 None => modified_files_from_rev(("@".into(), None), current), 804 } 805} 806 807pub fn modified_revision_or_range_files(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> { 808 if let Some(rev) = parse::revision() { 809 return modified_files_from_rev((rev, None), current); 810 } 811 modified_range_files(current) 812} 813 814pub fn revision_conflicted_files(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> { 815 conflicted_files_from_rev(&parse::revision_or_wc(), current) 816} 817 818/// Specific function for completing file paths for `jj squash` 819pub fn squash_revision_files(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> { 820 let rev = parse::squash_revision().unwrap_or_else(|| "@".into()); 821 modified_files_from_rev((rev, None), current) 822} 823 824/// Specific function for completing file paths for `jj interdiff` 825pub fn interdiff_files(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> { 826 let Some((from, to)) = parse::range() else { 827 return Vec::new(); 828 }; 829 // Complete all modified files in "from" and "to". This will also suggest 830 // files that are the same in both, which is a false positive. This approach 831 // is more lightweight than actually doing a temporary rebase here. 832 with_jj(|jj, _| { 833 let mut res = modified_files_from_rev_with_jj_cmd((from, None), jj.build(), current)?; 834 res.extend(modified_files_from_rev_with_jj_cmd( 835 (to, None), 836 jj.build(), 837 current, 838 )?); 839 Ok(res) 840 }) 841} 842 843/// Specific function for completing file paths for `jj log` 844pub fn log_files(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> { 845 let mut rev = parse::log_revisions().join(")|("); 846 if rev.is_empty() { 847 rev = "@".into(); 848 } else { 849 rev = format!("latest(heads(({rev})))"); // limit to one 850 }; 851 all_files_from_rev(rev, current) 852} 853 854/// Shell out to jj during dynamic completion generation 855/// 856/// In case of errors, print them and early return an empty vector. 857fn with_jj<F>(completion_fn: F) -> Vec<CompletionCandidate> 858where 859 F: FnOnce(JjBuilder, &UserSettings) -> Result<Vec<CompletionCandidate>, CommandError>, 860{ 861 get_jj_command() 862 .and_then(|(jj, settings)| completion_fn(jj, &settings)) 863 .unwrap_or_else(|e| { 864 eprintln!("{}", e.error); 865 Vec::new() 866 }) 867} 868 869/// Shell out to jj during dynamic completion generation 870/// 871/// This is necessary because dynamic completion code needs to be aware of 872/// global configuration like custom storage backends. Dynamic completion 873/// code via clap_complete doesn't accept arguments, so they cannot be passed 874/// that way. Another solution would've been to use global mutable state, to 875/// give completion code access to custom backends. Shelling out was chosen as 876/// the preferred method, because it's more maintainable and the performance 877/// requirements of completions aren't very high. 878fn get_jj_command() -> Result<(JjBuilder, UserSettings), CommandError> { 879 let current_exe = std::env::current_exe().map_err(user_error)?; 880 let mut cmd_args = Vec::<String>::new(); 881 882 // Snapshotting could make completions much slower in some situations 883 // and be undesired by the user. 884 cmd_args.push("--ignore-working-copy".into()); 885 cmd_args.push("--color=never".into()); 886 cmd_args.push("--no-pager".into()); 887 888 // Parse some of the global args we care about for passing along to the 889 // child process. This shouldn't fail, since none of the global args are 890 // required. 891 let app = crate::commands::default_app(); 892 let mut raw_config = config_from_environment(default_config_layers()); 893 let ui = Ui::null(); 894 let cwd = std::env::current_dir() 895 .and_then(dunce::canonicalize) 896 .map_err(user_error)?; 897 // No config migration for completion. Simply ignore deprecated variables. 898 let mut config_env = ConfigEnv::from_environment(&ui); 899 let maybe_cwd_workspace_loader = DefaultWorkspaceLoaderFactory.create(find_workspace_dir(&cwd)); 900 let _ = config_env.reload_user_config(&mut raw_config); 901 if let Ok(loader) = &maybe_cwd_workspace_loader { 902 config_env.reset_repo_path(loader.repo_path()); 903 let _ = config_env.reload_repo_config(&mut raw_config); 904 } 905 let mut config = config_env.resolve_config(&raw_config)?; 906 // skip 2 because of the clap_complete prelude: jj -- jj <actual args...> 907 let args = std::env::args_os().skip(2); 908 let args = expand_args(&ui, &app, args, &config)?; 909 let arg_matches = app 910 .clone() 911 .disable_version_flag(true) 912 .disable_help_flag(true) 913 .ignore_errors(true) 914 .try_get_matches_from(args)?; 915 let args: GlobalArgs = GlobalArgs::from_arg_matches(&arg_matches)?; 916 917 if let Some(repository) = args.repository { 918 // Try to update repo-specific config on a best-effort basis. 919 if let Ok(loader) = DefaultWorkspaceLoaderFactory.create(&cwd.join(&repository)) { 920 config_env.reset_repo_path(loader.repo_path()); 921 let _ = config_env.reload_repo_config(&mut raw_config); 922 if let Ok(new_config) = config_env.resolve_config(&raw_config) { 923 config = new_config; 924 } 925 } 926 cmd_args.push("--repository".into()); 927 cmd_args.push(repository); 928 } 929 if let Some(at_operation) = args.at_operation { 930 // We cannot assume that the value of at_operation is valid, because 931 // the user may be requesting completions precisely for this invalid 932 // operation ID. Additionally, the user may have mistyped the ID, 933 // in which case adding the argument blindly would break all other 934 // completions, even unrelated ones. 935 // 936 // To avoid this, we shell out to ourselves once with the argument 937 // and check the exit code. There is some performance overhead to this, 938 // but this code path is probably only executed in exceptional 939 // situations. 940 let mut canary_cmd = std::process::Command::new(&current_exe); 941 canary_cmd.args(&cmd_args); 942 canary_cmd.arg("--at-operation"); 943 canary_cmd.arg(&at_operation); 944 canary_cmd.arg("debug"); 945 canary_cmd.arg("snapshot"); 946 947 match canary_cmd.output() { 948 Ok(output) if output.status.success() => { 949 // Operation ID is valid, add it to the completion command. 950 cmd_args.push("--at-operation".into()); 951 cmd_args.push(at_operation); 952 } 953 _ => {} // Invalid operation ID, ignore. 954 } 955 } 956 for (kind, value) in args.early_args.merged_config_args(&arg_matches) { 957 let arg = match kind { 958 ConfigArgKind::Item => format!("--config={value}"), 959 ConfigArgKind::Toml => format!("--config-toml={value}"), 960 ConfigArgKind::File => format!("--config-file={value}"), 961 }; 962 cmd_args.push(arg); 963 } 964 965 let builder = JjBuilder { 966 cmd: current_exe, 967 args: cmd_args, 968 }; 969 let settings = UserSettings::from_config(config)?; 970 971 Ok((builder, settings)) 972} 973 974/// A helper struct to allow completion functions to call jj multiple times with 975/// different arguments. 976struct JjBuilder { 977 cmd: std::path::PathBuf, 978 args: Vec<String>, 979} 980 981impl JjBuilder { 982 fn build(&self) -> std::process::Command { 983 let mut cmd = std::process::Command::new(&self.cmd); 984 cmd.args(&self.args); 985 cmd 986 } 987} 988 989/// Functions for parsing revisions and revision ranges from the command line. 990/// Parsing is done on a best-effort basis and relies on the heuristic that 991/// most command line flags are consistent across different subcommands. 992/// 993/// In some cases, this parsing will be incorrect, but it's not worth the effort 994/// to fix that. For example, if the user specifies any of the relevant flags 995/// multiple times, the parsing will pick any of the available ones, while the 996/// actual execution of the command would fail. 997mod parse { 998 pub(super) fn parse_flag<'a, I: Iterator<Item = String>>( 999 candidates: &'a [&str], 1000 mut args: I, 1001 ) -> impl Iterator<Item = String> + use<'a, I> { 1002 std::iter::from_fn(move || { 1003 for arg in args.by_ref() { 1004 // -r REV syntax 1005 if candidates.contains(&arg.as_ref()) { 1006 match args.next() { 1007 Some(val) if !val.starts_with('-') => return Some(val), 1008 _ => return None, 1009 } 1010 } 1011 1012 // -r=REV syntax 1013 if let Some(value) = candidates.iter().find_map(|candidate| { 1014 let rest = arg.strip_prefix(candidate)?; 1015 match rest.strip_prefix('=') { 1016 Some(value) => Some(value), 1017 1018 // -rREV syntax 1019 None if candidate.len() == 2 => Some(rest), 1020 1021 None => None, 1022 } 1023 }) { 1024 return Some(value.into()); 1025 }; 1026 } 1027 None 1028 }) 1029 } 1030 1031 pub fn parse_revision_impl(args: impl Iterator<Item = String>) -> Option<String> { 1032 parse_flag(&["-r", "--revision"], args).next() 1033 } 1034 1035 pub fn revision() -> Option<String> { 1036 parse_revision_impl(std::env::args()) 1037 } 1038 1039 pub fn revision_or_wc() -> String { 1040 revision().unwrap_or_else(|| "@".into()) 1041 } 1042 1043 pub fn parse_range_impl<T>(args: impl Fn() -> T) -> Option<(String, String)> 1044 where 1045 T: Iterator<Item = String>, 1046 { 1047 let from = parse_flag(&["-f", "--from"], args()).next()?; 1048 let to = parse_flag(&["-t", "--to"], args()) 1049 .next() 1050 .unwrap_or_else(|| "@".into()); 1051 1052 Some((from, to)) 1053 } 1054 1055 pub fn range() -> Option<(String, String)> { 1056 parse_range_impl(std::env::args) 1057 } 1058 1059 // Special parse function only for `jj squash`. While squash has --from and 1060 // --to arguments, only files within --from should be completed, because 1061 // the files changed only in some other revision in the range between 1062 // --from and --to cannot be squashed into --to like that. 1063 pub fn squash_revision() -> Option<String> { 1064 if let Some(rev) = parse_flag(&["-r", "--revision"], std::env::args()).next() { 1065 return Some(rev); 1066 } 1067 parse_flag(&["-f", "--from"], std::env::args()).next() 1068 } 1069 1070 // Special parse function only for `jj log`. It has a --revisions flag, 1071 // instead of the usual --revision, and it can be supplied multiple times. 1072 pub fn log_revisions() -> Vec<String> { 1073 let candidates = &["-r", "--revisions"]; 1074 parse_flag(candidates, std::env::args()).collect() 1075 } 1076} 1077 1078#[cfg(test)] 1079mod tests { 1080 use super::*; 1081 1082 #[test] 1083 fn test_split_revset_trailing_name() { 1084 assert_eq!(split_revset_trailing_name(""), Some(("", ""))); 1085 assert_eq!(split_revset_trailing_name(" "), Some((" ", ""))); 1086 assert_eq!(split_revset_trailing_name("foo"), Some(("", "foo"))); 1087 assert_eq!(split_revset_trailing_name(" foo"), Some((" ", "foo"))); 1088 assert_eq!(split_revset_trailing_name("foo "), None); 1089 assert_eq!(split_revset_trailing_name("foo_"), Some(("", "foo_"))); 1090 assert_eq!(split_revset_trailing_name("foo/"), Some(("", "foo/"))); 1091 assert_eq!(split_revset_trailing_name("foo/b"), Some(("", "foo/b"))); 1092 1093 assert_eq!(split_revset_trailing_name("foo-"), Some(("", "foo-"))); 1094 assert_eq!(split_revset_trailing_name("foo+"), Some(("", "foo+"))); 1095 assert_eq!( 1096 split_revset_trailing_name("foo-bar-"), 1097 Some(("", "foo-bar-")) 1098 ); 1099 assert_eq!( 1100 split_revset_trailing_name("foo-bar-b"), 1101 Some(("", "foo-bar-b")) 1102 ); 1103 1104 assert_eq!(split_revset_trailing_name("foo."), Some(("", "foo."))); 1105 assert_eq!(split_revset_trailing_name("foo..b"), Some(("foo..", "b"))); 1106 assert_eq!(split_revset_trailing_name("..foo"), Some(("..", "foo"))); 1107 1108 assert_eq!(split_revset_trailing_name("foo(bar"), Some(("foo(", "bar"))); 1109 assert_eq!(split_revset_trailing_name("foo(bar)"), None); 1110 assert_eq!(split_revset_trailing_name("(f"), Some(("(", "f"))); 1111 1112 assert_eq!(split_revset_trailing_name("foo@"), Some(("", "foo@"))); 1113 assert_eq!(split_revset_trailing_name("foo@b"), Some(("", "foo@b"))); 1114 assert_eq!(split_revset_trailing_name("..foo@"), Some(("..", "foo@"))); 1115 assert_eq!( 1116 split_revset_trailing_name("::F(foo@origin.1..bar@origin."), 1117 Some(("::F(foo@origin.1..", "bar@origin.")) 1118 ); 1119 } 1120 1121 #[test] 1122 fn test_split_revset_trailing_name_with_trailing_operator() { 1123 assert_eq!(split_revset_trailing_name("foo|"), Some(("foo|", ""))); 1124 assert_eq!(split_revset_trailing_name("foo | "), Some(("foo | ", ""))); 1125 assert_eq!(split_revset_trailing_name("foo&"), Some(("foo&", ""))); 1126 assert_eq!(split_revset_trailing_name("foo~"), Some(("foo~", ""))); 1127 1128 assert_eq!(split_revset_trailing_name(".."), Some(("..", ""))); 1129 assert_eq!(split_revset_trailing_name("foo.."), Some(("foo..", ""))); 1130 assert_eq!(split_revset_trailing_name("::"), Some(("::", ""))); 1131 assert_eq!(split_revset_trailing_name("foo::"), Some(("foo::", ""))); 1132 1133 assert_eq!(split_revset_trailing_name("("), Some(("(", ""))); 1134 assert_eq!(split_revset_trailing_name("foo("), Some(("foo(", ""))); 1135 assert_eq!(split_revset_trailing_name("foo()"), None); 1136 assert_eq!(split_revset_trailing_name("foo(bar)"), None); 1137 } 1138 1139 #[test] 1140 fn test_split_revset_trailing_name_with_modifier() { 1141 assert_eq!(split_revset_trailing_name("all:"), Some(("all:", ""))); 1142 assert_eq!(split_revset_trailing_name("all: "), Some(("all: ", ""))); 1143 assert_eq!(split_revset_trailing_name("all:f"), Some(("all:", "f"))); 1144 assert_eq!(split_revset_trailing_name("all: f"), Some(("all: ", "f"))); 1145 } 1146 1147 #[test] 1148 fn test_config_keys() { 1149 // Just make sure the schema is parsed without failure. 1150 let _ = config_keys(); 1151 } 1152 1153 #[test] 1154 fn test_parse_revision_impl() { 1155 let good_cases: &[&[&str]] = &[ 1156 &["-r", "foo"], 1157 &["--revision", "foo"], 1158 &["-r=foo"], 1159 &["--revision=foo"], 1160 &["preceding_arg", "-r", "foo"], 1161 &["-r", "foo", "following_arg"], 1162 ]; 1163 for case in good_cases { 1164 let args = case.iter().map(|s| s.to_string()); 1165 assert_eq!( 1166 parse::parse_revision_impl(args), 1167 Some("foo".into()), 1168 "case: {case:?}", 1169 ); 1170 } 1171 let bad_cases: &[&[&str]] = &[&[], &["-r"], &["foo"], &["-R", "foo"], &["-R=foo"]]; 1172 for case in bad_cases { 1173 let args = case.iter().map(|s| s.to_string()); 1174 assert_eq!(parse::parse_revision_impl(args), None, "case: {case:?}"); 1175 } 1176 } 1177 1178 #[test] 1179 fn test_parse_range_impl() { 1180 let wc_cases: &[&[&str]] = &[ 1181 &["-f", "foo"], 1182 &["--from", "foo"], 1183 &["-f=foo"], 1184 &["preceding_arg", "-f", "foo"], 1185 &["-f", "foo", "following_arg"], 1186 ]; 1187 for case in wc_cases { 1188 let args = case.iter().map(|s| s.to_string()); 1189 assert_eq!( 1190 parse::parse_range_impl(|| args.clone()), 1191 Some(("foo".into(), "@".into())), 1192 "case: {case:?}", 1193 ); 1194 } 1195 let to_cases: &[&[&str]] = &[ 1196 &["-f", "foo", "-t", "bar"], 1197 &["-f", "foo", "--to", "bar"], 1198 &["-f=foo", "-t=bar"], 1199 &["-t=bar", "-f=foo"], 1200 ]; 1201 for case in to_cases { 1202 let args = case.iter().map(|s| s.to_string()); 1203 assert_eq!( 1204 parse::parse_range_impl(|| args.clone()), 1205 Some(("foo".into(), "bar".into())), 1206 "case: {case:?}", 1207 ); 1208 } 1209 let bad_cases: &[&[&str]] = &[&[], &["-f"], &["foo"], &["-R", "foo"], &["-R=foo"]]; 1210 for case in bad_cases { 1211 let args = case.iter().map(|s| s.to_string()); 1212 assert_eq!( 1213 parse::parse_range_impl(|| args.clone()), 1214 None, 1215 "case: {case:?}" 1216 ); 1217 } 1218 } 1219 1220 #[test] 1221 fn test_parse_multiple_flags() { 1222 let candidates = &["-r", "--revisions"]; 1223 let args = &[ 1224 "unrelated_arg_at_the_beginning", 1225 "-r", 1226 "1", 1227 "--revisions", 1228 "2", 1229 "-r=3", 1230 "--revisions=4", 1231 "unrelated_arg_in_the_middle", 1232 "-r5", 1233 "unrelated_arg_at_the_end", 1234 ]; 1235 let flags: Vec<_> = 1236 parse::parse_flag(candidates, args.iter().map(|a| a.to_string())).collect(); 1237 let expected = ["1", "2", "3", "4", "5"]; 1238 assert_eq!(flags, expected); 1239 } 1240}