just playing with tangled
at gvimdiff 38 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(revisions: 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) = revisions { 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(stdout.lines().map(|line| { 267 let (bookmark, help) = split_help_text(line); 268 269 let local = !bookmark.contains('@'); 270 let mine = prefix.as_ref().is_some_and(|p| bookmark.starts_with(p)); 271 272 let display_order = match (local, mine) { 273 (true, true) => LOCAL_BOOKMARK_MINE, 274 (true, false) => LOCAL_BOOKMARK, 275 (false, true) => REMOTE_BOOKMARK_MINE, 276 (false, false) => REMOTE_BOOKMARK, 277 }; 278 CompletionCandidate::new(bookmark) 279 .help(help) 280 .display_order(Some(display_order)) 281 })); 282 283 // tags 284 285 // Tags cannot be filtered by revisions. In order to avoid suggesting 286 // immutable tags for mutable revision args, we skip tags entirely if 287 // revisions is set. This is not a big loss, since tags usually point 288 // to immutable revisions anyway. 289 if revisions.is_none() { 290 let output = jj 291 .build() 292 .arg("tag") 293 .arg("list") 294 .arg("--config") 295 .arg(BOOKMARK_HELP_TEMPLATE) 296 .arg("--template") 297 .arg(r#"name ++ bookmark_help() ++ "\n""#) 298 .output() 299 .map_err(user_error)?; 300 let stdout = String::from_utf8_lossy(&output.stdout); 301 302 candidates.extend(stdout.lines().map(|line| { 303 let (name, desc) = split_help_text(line); 304 CompletionCandidate::new(name) 305 .help(desc) 306 .display_order(Some(TAG)) 307 })); 308 } 309 310 // change IDs 311 312 let revisions = revisions 313 .map(String::from) 314 .or_else(|| settings.get_string("revsets.short-prefixes").ok()) 315 .or_else(|| settings.get_string("revsets.log").ok()) 316 .unwrap_or_default(); 317 318 let output = jj 319 .build() 320 .arg("log") 321 .arg("--no-graph") 322 .arg("--limit") 323 .arg("100") 324 .arg("--revisions") 325 .arg(revisions) 326 .arg("--template") 327 .arg(r#"change_id.shortest() ++ " " ++ if(description, description.first_line(), "(no description set)") ++ "\n""#) 328 .output() 329 .map_err(user_error)?; 330 let stdout = String::from_utf8_lossy(&output.stdout); 331 332 candidates.extend(stdout.lines().map(|line| { 333 let (id, desc) = split_help_text(line); 334 CompletionCandidate::new(id) 335 .help(desc) 336 .display_order(Some(CHANGE_ID)) 337 })); 338 339 // revset aliases 340 341 let revset_aliases = load_revset_aliases(&Ui::null(), settings.config())?; 342 let mut symbol_names: Vec<_> = revset_aliases.symbol_names().collect(); 343 symbol_names.sort(); 344 candidates.extend(symbol_names.into_iter().map(|symbol| { 345 let (_, defn) = revset_aliases.get_symbol(symbol).unwrap(); 346 CompletionCandidate::new(symbol) 347 .help(Some(defn.into())) 348 .display_order(Some(REVSET_ALIAS)) 349 })); 350 351 Ok(candidates) 352 }) 353} 354 355pub fn mutable_revisions() -> Vec<CompletionCandidate> { 356 revisions(Some("mutable()")) 357} 358 359pub fn all_revisions() -> Vec<CompletionCandidate> { 360 revisions(None) 361} 362 363pub fn operations() -> Vec<CompletionCandidate> { 364 with_jj(|jj, _| { 365 let output = jj 366 .build() 367 .arg("operation") 368 .arg("log") 369 .arg("--no-graph") 370 .arg("--limit") 371 .arg("100") 372 .arg("--template") 373 .arg( 374 r#" 375 separate(" ", 376 id.short(), 377 "(" ++ format_timestamp(time.end()) ++ ")", 378 description.first_line(), 379 ) ++ "\n""#, 380 ) 381 .output() 382 .map_err(user_error)?; 383 384 Ok(String::from_utf8_lossy(&output.stdout) 385 .lines() 386 .map(|line| { 387 let (id, help) = split_help_text(line); 388 CompletionCandidate::new(id).help(help) 389 }) 390 .collect()) 391 }) 392} 393 394pub fn workspaces() -> Vec<CompletionCandidate> { 395 with_jj(|jj, _| { 396 let output = jj 397 .build() 398 .arg("--config") 399 .arg(r#"templates.commit_summary='if(description, description.first_line(), "(no description set)")'"#) 400 .arg("workspace") 401 .arg("list") 402 .output() 403 .map_err(user_error)?; 404 let stdout = String::from_utf8_lossy(&output.stdout); 405 406 Ok(stdout 407 .lines() 408 .map(|line| { 409 let (name, desc) = line.split_once(": ").unwrap_or((line, "")); 410 CompletionCandidate::new(name).help(Some(desc.to_string().into())) 411 }) 412 .collect()) 413 }) 414} 415 416fn config_keys_rec( 417 prefix: ConfigNamePathBuf, 418 properties: &serde_json::Map<String, serde_json::Value>, 419 acc: &mut Vec<CompletionCandidate>, 420 only_leaves: bool, 421 suffix: &str, 422) { 423 for (key, value) in properties { 424 let mut prefix = prefix.clone(); 425 prefix.push(key); 426 427 let value = value.as_object().unwrap(); 428 match value.get("type").and_then(|v| v.as_str()) { 429 Some("object") => { 430 if !only_leaves { 431 let help = value 432 .get("description") 433 .map(|desc| desc.as_str().unwrap().to_string().into()); 434 let escaped_key = prefix.to_string(); 435 acc.push(CompletionCandidate::new(escaped_key).help(help)); 436 } 437 let Some(properties) = value.get("properties") else { 438 continue; 439 }; 440 let properties = properties.as_object().unwrap(); 441 config_keys_rec(prefix, properties, acc, only_leaves, suffix); 442 } 443 _ => { 444 let help = value 445 .get("description") 446 .map(|desc| desc.as_str().unwrap().to_string().into()); 447 let escaped_key = format!("{prefix}{suffix}"); 448 acc.push(CompletionCandidate::new(escaped_key).help(help)); 449 } 450 } 451 } 452} 453 454fn json_keypath<'a>( 455 schema: &'a serde_json::Value, 456 keypath: &str, 457 separator: &str, 458) -> Option<&'a serde_json::Value> { 459 keypath 460 .split(separator) 461 .try_fold(schema, |value, step| value.get(step)) 462} 463fn jsonschema_keypath<'a>( 464 schema: &'a serde_json::Value, 465 keypath: &ConfigNamePathBuf, 466) -> Option<&'a serde_json::Value> { 467 keypath.components().try_fold(schema, |value, step| { 468 let value = value.as_object()?; 469 if value.get("type")?.as_str()? != "object" { 470 return None; 471 } 472 let properties = value.get("properties")?.as_object()?; 473 properties.get(step.get()) 474 }) 475} 476 477fn config_values(path: &ConfigNamePathBuf) -> Option<Vec<String>> { 478 let schema: serde_json::Value = serde_json::from_str(CONFIG_SCHEMA).unwrap(); 479 480 let mut config_entry = jsonschema_keypath(&schema, path)?; 481 if let Some(reference) = config_entry.get("$ref") { 482 let reference = reference.as_str()?.strip_prefix("#/")?; 483 config_entry = json_keypath(&schema, reference, "/")?; 484 }; 485 486 if let Some(possibile_values) = config_entry.get("enum") { 487 return Some( 488 possibile_values 489 .as_array()? 490 .iter() 491 .filter_map(|val| val.as_str()) 492 .map(ToOwned::to_owned) 493 .collect(), 494 ); 495 } 496 497 Some(match config_entry.get("type")?.as_str()? { 498 "boolean" => vec!["false".into(), "true".into()], 499 _ => vec![], 500 }) 501} 502 503fn config_keys_impl(only_leaves: bool, suffix: &str) -> Vec<CompletionCandidate> { 504 let schema: serde_json::Value = serde_json::from_str(CONFIG_SCHEMA).unwrap(); 505 let schema = schema.as_object().unwrap(); 506 let properties = schema["properties"].as_object().unwrap(); 507 508 let mut candidates = Vec::new(); 509 config_keys_rec( 510 ConfigNamePathBuf::root(), 511 properties, 512 &mut candidates, 513 only_leaves, 514 suffix, 515 ); 516 candidates 517} 518 519pub fn config_keys() -> Vec<CompletionCandidate> { 520 config_keys_impl(false, "") 521} 522 523pub fn leaf_config_keys() -> Vec<CompletionCandidate> { 524 config_keys_impl(true, "") 525} 526 527pub fn leaf_config_key_value(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> { 528 let Some(current) = current.to_str() else { 529 return Vec::new(); 530 }; 531 532 if let Some((key, current_val)) = current.split_once('=') { 533 let Ok(key) = key.parse() else { 534 return Vec::new(); 535 }; 536 let possible_values = config_values(&key).unwrap_or_default(); 537 538 possible_values 539 .into_iter() 540 .filter(|x| x.starts_with(current_val)) 541 .map(|x| CompletionCandidate::new(format!("{key}={x}"))) 542 .collect() 543 } else { 544 config_keys_impl(true, "=") 545 .into_iter() 546 .filter(|candidate| candidate.get_value().to_str().unwrap().starts_with(current)) 547 .collect() 548 } 549} 550 551pub fn branch_name_equals_any_revision(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> { 552 let Some(current) = current.to_str() else { 553 return Vec::new(); 554 }; 555 556 let Some((branch_name, revision)) = current.split_once('=') else { 557 // Don't complete branch names since we want to create a new branch 558 return Vec::new(); 559 }; 560 all_revisions() 561 .into_iter() 562 .filter(|rev| { 563 rev.get_value() 564 .to_str() 565 .is_some_and(|s| s.starts_with(revision)) 566 }) 567 .map(|rev| rev.add_prefix(format!("{branch_name}="))) 568 .collect() 569} 570 571fn dir_prefix_from<'a>(path: &'a str, current: &str) -> Option<&'a str> { 572 path[current.len()..] 573 .split_once(std::path::MAIN_SEPARATOR) 574 .map(|(next, _)| path.split_at(current.len() + next.len() + 1).0) 575} 576 577fn current_prefix_to_fileset(current: &str) -> String { 578 let cur_esc = glob::Pattern::escape(current); 579 let dir_pat = format!("{cur_esc}*/**"); 580 let path_pat = format!("{cur_esc}*"); 581 format!("glob:{dir_pat:?} | glob:{path_pat:?}") 582} 583 584fn all_files_from_rev(rev: String, current: &std::ffi::OsStr) -> Vec<CompletionCandidate> { 585 let Some(current) = current.to_str() else { 586 return Vec::new(); 587 }; 588 with_jj(|jj, _| { 589 let mut child = jj 590 .build() 591 .arg("file") 592 .arg("list") 593 .arg("--revision") 594 .arg(rev) 595 .arg(current_prefix_to_fileset(current)) 596 .stdout(std::process::Stdio::piped()) 597 .stderr(std::process::Stdio::null()) 598 .spawn() 599 .map_err(user_error)?; 600 let stdout = child.stdout.take().unwrap(); 601 602 Ok(std::io::BufReader::new(stdout) 603 .lines() 604 .take(1_000) 605 .map_while(Result::ok) 606 .map(|path| { 607 if let Some(dir_path) = dir_prefix_from(&path, current) { 608 return CompletionCandidate::new(dir_path); 609 } 610 CompletionCandidate::new(path) 611 }) 612 .dedup() // directories may occur multiple times 613 .collect()) 614 }) 615} 616 617fn modified_files_from_rev_with_jj_cmd( 618 rev: (String, Option<String>), 619 mut cmd: std::process::Command, 620 current: &std::ffi::OsStr, 621) -> Result<Vec<CompletionCandidate>, CommandError> { 622 let Some(current) = current.to_str() else { 623 return Ok(Vec::new()); 624 }; 625 cmd.arg("diff") 626 .arg("--summary") 627 .arg(current_prefix_to_fileset(current)); 628 match rev { 629 (rev, None) => cmd.arg("--revisions").arg(rev), 630 (from, Some(to)) => cmd.arg("--from").arg(from).arg("--to").arg(to), 631 }; 632 let output = cmd.output().map_err(user_error)?; 633 let stdout = String::from_utf8_lossy(&output.stdout); 634 635 let mut candidates = Vec::new(); 636 // store renamed paths in a separate vector so we don't have to sort later 637 let mut renamed = Vec::new(); 638 639 'line_loop: for line in stdout.lines() { 640 let (mode, path) = line 641 .split_once(' ') 642 .expect("diff --summary should contain a space between mode and path"); 643 644 fn path_to_candidate(current: &str, mode: &str, p: impl AsRef<str>) -> CompletionCandidate { 645 let p = p.as_ref(); 646 if let Some(dir_path) = dir_prefix_from(p, current) { 647 return CompletionCandidate::new(dir_path); 648 } 649 650 let help = match mode { 651 "M" => "Modified".into(), 652 "D" => "Deleted".into(), 653 "A" => "Added".into(), 654 "R" => "Renamed".into(), 655 "C" => "Copied".into(), 656 _ => format!("unknown mode: '{mode}'"), 657 }; 658 CompletionCandidate::new(p).help(Some(help.into())) 659 } 660 661 // In case of a rename, one line of `diff --summary` results in 662 // two suggestions. 663 if mode == "R" { 664 'split_renamed_paths: { 665 let Some((prefix, rest)) = path.split_once('{') else { 666 break 'split_renamed_paths; 667 }; 668 let Some((rename, suffix)) = rest.split_once('}') else { 669 break 'split_renamed_paths; 670 }; 671 let Some((before, after)) = rename.split_once(" => ") else { 672 break 'split_renamed_paths; 673 }; 674 let before = format!("{prefix}{before}{suffix}"); 675 let after = format!("{prefix}{after}{suffix}"); 676 candidates.push(path_to_candidate(current, mode, before)); 677 renamed.push(path_to_candidate(current, mode, after)); 678 continue 'line_loop; 679 }; 680 } 681 682 candidates.push(path_to_candidate(current, mode, path)); 683 } 684 candidates.extend(renamed); 685 candidates.dedup(); 686 687 Ok(candidates) 688} 689 690fn modified_files_from_rev( 691 rev: (String, Option<String>), 692 current: &std::ffi::OsStr, 693) -> Vec<CompletionCandidate> { 694 with_jj(|jj, _| modified_files_from_rev_with_jj_cmd(rev, jj.build(), current)) 695} 696 697fn conflicted_files_from_rev(rev: &str, current: &std::ffi::OsStr) -> Vec<CompletionCandidate> { 698 let Some(current) = current.to_str() else { 699 return Vec::new(); 700 }; 701 with_jj(|jj, _| { 702 let output = jj 703 .build() 704 .arg("resolve") 705 .arg("--list") 706 .arg("--revision") 707 .arg(rev) 708 .arg(current_prefix_to_fileset(current)) 709 .output() 710 .map_err(user_error)?; 711 let stdout = String::from_utf8_lossy(&output.stdout); 712 713 Ok(stdout 714 .lines() 715 .map(|line| { 716 let path = line 717 .split_whitespace() 718 .next() 719 .expect("resolve --list should contain whitespace after path"); 720 721 if let Some(dir_path) = dir_prefix_from(path, current) { 722 return CompletionCandidate::new(dir_path); 723 } 724 CompletionCandidate::new(path) 725 }) 726 .dedup() // directories may occur multiple times 727 .collect()) 728 }) 729} 730 731pub fn modified_files(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> { 732 modified_files_from_rev(("@".into(), None), current) 733} 734 735pub fn all_revision_files(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> { 736 all_files_from_rev(parse::revision_or_wc(), current) 737} 738 739pub fn modified_revision_files(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> { 740 modified_files_from_rev((parse::revision_or_wc(), None), current) 741} 742 743pub fn modified_range_files(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> { 744 match parse::range() { 745 Some((from, to)) => modified_files_from_rev((from, Some(to)), current), 746 None => modified_files_from_rev(("@".into(), None), current), 747 } 748} 749 750pub fn modified_revision_or_range_files(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> { 751 if let Some(rev) = parse::revision() { 752 return modified_files_from_rev((rev, None), current); 753 } 754 modified_range_files(current) 755} 756 757pub fn revision_conflicted_files(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> { 758 conflicted_files_from_rev(&parse::revision_or_wc(), current) 759} 760 761/// Specific function for completing file paths for `jj squash` 762pub fn squash_revision_files(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> { 763 let rev = parse::squash_revision().unwrap_or_else(|| "@".into()); 764 modified_files_from_rev((rev, None), current) 765} 766 767/// Specific function for completing file paths for `jj interdiff` 768pub fn interdiff_files(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> { 769 let Some((from, to)) = parse::range() else { 770 return Vec::new(); 771 }; 772 // Complete all modified files in "from" and "to". This will also suggest 773 // files that are the same in both, which is a false positive. This approach 774 // is more lightweight than actually doing a temporary rebase here. 775 with_jj(|jj, _| { 776 let mut res = modified_files_from_rev_with_jj_cmd((from, None), jj.build(), current)?; 777 res.extend(modified_files_from_rev_with_jj_cmd( 778 (to, None), 779 jj.build(), 780 current, 781 )?); 782 Ok(res) 783 }) 784} 785 786/// Specific function for completing file paths for `jj log` 787pub fn log_files(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> { 788 let mut rev = parse::log_revisions().join(")|("); 789 if rev.is_empty() { 790 rev = "@".into(); 791 } else { 792 rev = format!("latest(heads(({rev})))"); // limit to one 793 }; 794 all_files_from_rev(rev, current) 795} 796 797/// Shell out to jj during dynamic completion generation 798/// 799/// In case of errors, print them and early return an empty vector. 800fn with_jj<F>(completion_fn: F) -> Vec<CompletionCandidate> 801where 802 F: FnOnce(JjBuilder, &UserSettings) -> Result<Vec<CompletionCandidate>, CommandError>, 803{ 804 get_jj_command() 805 .and_then(|(jj, settings)| completion_fn(jj, &settings)) 806 .unwrap_or_else(|e| { 807 eprintln!("{}", e.error); 808 Vec::new() 809 }) 810} 811 812/// Shell out to jj during dynamic completion generation 813/// 814/// This is necessary because dynamic completion code needs to be aware of 815/// global configuration like custom storage backends. Dynamic completion 816/// code via clap_complete doesn't accept arguments, so they cannot be passed 817/// that way. Another solution would've been to use global mutable state, to 818/// give completion code access to custom backends. Shelling out was chosen as 819/// the preferred method, because it's more maintainable and the performance 820/// requirements of completions aren't very high. 821fn get_jj_command() -> Result<(JjBuilder, UserSettings), CommandError> { 822 let current_exe = std::env::current_exe().map_err(user_error)?; 823 let mut cmd_args = Vec::<String>::new(); 824 825 // Snapshotting could make completions much slower in some situations 826 // and be undesired by the user. 827 cmd_args.push("--ignore-working-copy".into()); 828 cmd_args.push("--color=never".into()); 829 cmd_args.push("--no-pager".into()); 830 831 // Parse some of the global args we care about for passing along to the 832 // child process. This shouldn't fail, since none of the global args are 833 // required. 834 let app = crate::commands::default_app(); 835 let mut raw_config = config_from_environment(default_config_layers()); 836 let ui = Ui::null(); 837 let cwd = std::env::current_dir() 838 .and_then(dunce::canonicalize) 839 .map_err(user_error)?; 840 // No config migration for completion. Simply ignore deprecated variables. 841 let mut config_env = ConfigEnv::from_environment(); 842 let maybe_cwd_workspace_loader = DefaultWorkspaceLoaderFactory.create(find_workspace_dir(&cwd)); 843 let _ = config_env.reload_user_config(&mut raw_config); 844 if let Ok(loader) = &maybe_cwd_workspace_loader { 845 config_env.reset_repo_path(loader.repo_path()); 846 let _ = config_env.reload_repo_config(&mut raw_config); 847 } 848 let mut config = config_env.resolve_config(&raw_config)?; 849 // skip 2 because of the clap_complete prelude: jj -- jj <actual args...> 850 let args = std::env::args_os().skip(2); 851 let args = expand_args(&ui, &app, args, &config)?; 852 let arg_matches = app 853 .clone() 854 .disable_version_flag(true) 855 .disable_help_flag(true) 856 .ignore_errors(true) 857 .try_get_matches_from(args)?; 858 let args: GlobalArgs = GlobalArgs::from_arg_matches(&arg_matches)?; 859 860 if let Some(repository) = args.repository { 861 // Try to update repo-specific config on a best-effort basis. 862 if let Ok(loader) = DefaultWorkspaceLoaderFactory.create(&cwd.join(&repository)) { 863 config_env.reset_repo_path(loader.repo_path()); 864 let _ = config_env.reload_repo_config(&mut raw_config); 865 if let Ok(new_config) = config_env.resolve_config(&raw_config) { 866 config = new_config; 867 } 868 } 869 cmd_args.push("--repository".into()); 870 cmd_args.push(repository); 871 } 872 if let Some(at_operation) = args.at_operation { 873 // We cannot assume that the value of at_operation is valid, because 874 // the user may be requesting completions precisely for this invalid 875 // operation ID. Additionally, the user may have mistyped the ID, 876 // in which case adding the argument blindly would break all other 877 // completions, even unrelated ones. 878 // 879 // To avoid this, we shell out to ourselves once with the argument 880 // and check the exit code. There is some performance overhead to this, 881 // but this code path is probably only executed in exceptional 882 // situations. 883 let mut canary_cmd = std::process::Command::new(&current_exe); 884 canary_cmd.args(&cmd_args); 885 canary_cmd.arg("--at-operation"); 886 canary_cmd.arg(&at_operation); 887 canary_cmd.arg("debug"); 888 canary_cmd.arg("snapshot"); 889 890 match canary_cmd.output() { 891 Ok(output) if output.status.success() => { 892 // Operation ID is valid, add it to the completion command. 893 cmd_args.push("--at-operation".into()); 894 cmd_args.push(at_operation); 895 } 896 _ => {} // Invalid operation ID, ignore. 897 } 898 } 899 for (kind, value) in args.early_args.merged_config_args(&arg_matches) { 900 let arg = match kind { 901 ConfigArgKind::Item => format!("--config={value}"), 902 ConfigArgKind::Toml => format!("--config-toml={value}"), 903 ConfigArgKind::File => format!("--config-file={value}"), 904 }; 905 cmd_args.push(arg); 906 } 907 908 let builder = JjBuilder { 909 cmd: current_exe, 910 args: cmd_args, 911 }; 912 let settings = UserSettings::from_config(config)?; 913 914 Ok((builder, settings)) 915} 916 917/// A helper struct to allow completion functions to call jj multiple times with 918/// different arguments. 919struct JjBuilder { 920 cmd: std::path::PathBuf, 921 args: Vec<String>, 922} 923 924impl JjBuilder { 925 fn build(&self) -> std::process::Command { 926 let mut cmd = std::process::Command::new(&self.cmd); 927 cmd.args(&self.args); 928 cmd 929 } 930} 931 932/// Functions for parsing revisions and revision ranges from the command line. 933/// Parsing is done on a best-effort basis and relies on the heuristic that 934/// most command line flags are consistent across different subcommands. 935/// 936/// In some cases, this parsing will be incorrect, but it's not worth the effort 937/// to fix that. For example, if the user specifies any of the relevant flags 938/// multiple times, the parsing will pick any of the available ones, while the 939/// actual execution of the command would fail. 940mod parse { 941 pub(super) fn parse_flag<'a, I: Iterator<Item = String>>( 942 candidates: &'a [&str], 943 mut args: I, 944 ) -> impl Iterator<Item = String> + use<'a, I> { 945 std::iter::from_fn(move || { 946 for arg in args.by_ref() { 947 // -r REV syntax 948 if candidates.contains(&arg.as_ref()) { 949 match args.next() { 950 Some(val) if !val.starts_with('-') => return Some(val), 951 _ => return None, 952 } 953 } 954 955 // -r=REV syntax 956 if let Some(value) = candidates.iter().find_map(|candidate| { 957 let rest = arg.strip_prefix(candidate)?; 958 match rest.strip_prefix('=') { 959 Some(value) => Some(value), 960 961 // -rREV syntax 962 None if candidate.len() == 2 => Some(rest), 963 964 None => None, 965 } 966 }) { 967 return Some(value.into()); 968 }; 969 } 970 None 971 }) 972 } 973 974 pub fn parse_revision_impl(args: impl Iterator<Item = String>) -> Option<String> { 975 parse_flag(&["-r", "--revision"], args).next() 976 } 977 978 pub fn revision() -> Option<String> { 979 parse_revision_impl(std::env::args()) 980 } 981 982 pub fn revision_or_wc() -> String { 983 revision().unwrap_or_else(|| "@".into()) 984 } 985 986 pub fn parse_range_impl<T>(args: impl Fn() -> T) -> Option<(String, String)> 987 where 988 T: Iterator<Item = String>, 989 { 990 let from = parse_flag(&["-f", "--from"], args()).next()?; 991 let to = parse_flag(&["-t", "--to"], args()) 992 .next() 993 .unwrap_or_else(|| "@".into()); 994 995 Some((from, to)) 996 } 997 998 pub fn range() -> Option<(String, String)> { 999 parse_range_impl(std::env::args) 1000 } 1001 1002 // Special parse function only for `jj squash`. While squash has --from and 1003 // --to arguments, only files within --from should be completed, because 1004 // the files changed only in some other revision in the range between 1005 // --from and --to cannot be squashed into --to like that. 1006 pub fn squash_revision() -> Option<String> { 1007 if let Some(rev) = parse_flag(&["-r", "--revision"], std::env::args()).next() { 1008 return Some(rev); 1009 } 1010 parse_flag(&["-f", "--from"], std::env::args()).next() 1011 } 1012 1013 // Special parse function only for `jj log`. It has a --revisions flag, 1014 // instead of the usual --revision, and it can be supplied multiple times. 1015 pub fn log_revisions() -> Vec<String> { 1016 let candidates = &["-r", "--revisions"]; 1017 parse_flag(candidates, std::env::args()).collect() 1018 } 1019} 1020 1021#[cfg(test)] 1022mod tests { 1023 use super::*; 1024 1025 #[test] 1026 fn test_config_keys() { 1027 // Just make sure the schema is parsed without failure. 1028 let _ = config_keys(); 1029 } 1030 1031 #[test] 1032 fn test_parse_revision_impl() { 1033 let good_cases: &[&[&str]] = &[ 1034 &["-r", "foo"], 1035 &["--revision", "foo"], 1036 &["-r=foo"], 1037 &["--revision=foo"], 1038 &["preceding_arg", "-r", "foo"], 1039 &["-r", "foo", "following_arg"], 1040 ]; 1041 for case in good_cases { 1042 let args = case.iter().map(|s| s.to_string()); 1043 assert_eq!( 1044 parse::parse_revision_impl(args), 1045 Some("foo".into()), 1046 "case: {case:?}", 1047 ); 1048 } 1049 let bad_cases: &[&[&str]] = &[&[], &["-r"], &["foo"], &["-R", "foo"], &["-R=foo"]]; 1050 for case in bad_cases { 1051 let args = case.iter().map(|s| s.to_string()); 1052 assert_eq!(parse::parse_revision_impl(args), None, "case: {case:?}"); 1053 } 1054 } 1055 1056 #[test] 1057 fn test_parse_range_impl() { 1058 let wc_cases: &[&[&str]] = &[ 1059 &["-f", "foo"], 1060 &["--from", "foo"], 1061 &["-f=foo"], 1062 &["preceding_arg", "-f", "foo"], 1063 &["-f", "foo", "following_arg"], 1064 ]; 1065 for case in wc_cases { 1066 let args = case.iter().map(|s| s.to_string()); 1067 assert_eq!( 1068 parse::parse_range_impl(|| args.clone()), 1069 Some(("foo".into(), "@".into())), 1070 "case: {case:?}", 1071 ); 1072 } 1073 let to_cases: &[&[&str]] = &[ 1074 &["-f", "foo", "-t", "bar"], 1075 &["-f", "foo", "--to", "bar"], 1076 &["-f=foo", "-t=bar"], 1077 &["-t=bar", "-f=foo"], 1078 ]; 1079 for case in to_cases { 1080 let args = case.iter().map(|s| s.to_string()); 1081 assert_eq!( 1082 parse::parse_range_impl(|| args.clone()), 1083 Some(("foo".into(), "bar".into())), 1084 "case: {case:?}", 1085 ); 1086 } 1087 let bad_cases: &[&[&str]] = &[&[], &["-f"], &["foo"], &["-R", "foo"], &["-R=foo"]]; 1088 for case in bad_cases { 1089 let args = case.iter().map(|s| s.to_string()); 1090 assert_eq!( 1091 parse::parse_range_impl(|| args.clone()), 1092 None, 1093 "case: {case:?}" 1094 ); 1095 } 1096 } 1097 1098 #[test] 1099 fn test_parse_multiple_flags() { 1100 let candidates = &["-r", "--revisions"]; 1101 let args = &[ 1102 "unrelated_arg_at_the_beginning", 1103 "-r", 1104 "1", 1105 "--revisions", 1106 "2", 1107 "-r=3", 1108 "--revisions=4", 1109 "unrelated_arg_in_the_middle", 1110 "-r5", 1111 "unrelated_arg_at_the_end", 1112 ]; 1113 let flags: Vec<_> = 1114 parse::parse_flag(candidates, args.iter().map(|a| a.to_string())).collect(); 1115 let expected = ["1", "2", "3", "4", "5"]; 1116 assert_eq!(flags, expected); 1117 } 1118}