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
//! Harpoon — 9 pinned-file slots the user jumps to via `<leader>1`..`<leader>9`.
//! Pins live on `App.harpoon: [Option<PathBuf>; 9]` (workspace-relative
//! paths, deduped, persisted at quit through `session.json`).
//!
//! Extracted from `src/app/mod.rs`.
use crate::app::App;
use crate::app::util::rel_path;
use crate::picker::{Picker, PickerItem, PickerKind};
impl App {
/// Harpoon: pin the active editor's file into the lowest free slot
/// (1..=9). Toasts if the buffer has no path, the file is already
/// pinned, or every slot is full.
pub fn harpoon_add_active(&mut self) {
let Some(path) = self.active_editor().and_then(|b| b.path.clone()) else {
self.toast("harpoon: no file");
return;
};
if self.harpoon.iter().any(|s| s.as_ref() == Some(&path)) {
self.toast(format!(
"harpoon: already pinned ({})",
rel_path(&self.workspace, &path)
));
return;
}
if let Some(slot) = self.harpoon.iter_mut().position(|s| s.is_none()) {
self.harpoon[slot] = Some(path.clone());
self.toast(format!(
"harpoon: slot {} = {}",
slot + 1,
rel_path(&self.workspace, &path)
));
} else {
self.toast("harpoon: all 9 slots full (use harpoon.menu to free one)");
}
}
/// Harpoon: jump to slot N (1-based; the call sites `<leader>1`-`<leader>9`
/// pass the user's digit). Toasts if the slot is empty or the file
/// disappeared.
pub fn harpoon_goto(&mut self, slot1: usize) {
if !(1..=9).contains(&slot1) {
return;
}
let path = match self.harpoon[slot1 - 1].clone() {
Some(p) => p,
None => {
self.toast(format!("harpoon: slot {slot1} is empty"));
return;
}
};
if !path.exists() {
self.toast(format!(
"harpoon: slot {slot1} → file missing ({})",
path.display()
));
return;
}
self.open_path(&path);
}
/// Harpoon: open a picker over the occupied slots. Accept ⇒ jump to
/// that slot's pinned file. Toasts if every slot is empty.
pub fn harpoon_open_menu(&mut self) {
let items: Vec<PickerItem> = self
.harpoon
.iter()
.enumerate()
.filter_map(|(i, slot)| {
let path = slot.as_ref()?;
let rel = rel_path(&self.workspace, path);
let exists = path.exists();
let detail = if exists {
format!("slot {}", i + 1)
} else {
format!("slot {} · missing", i + 1)
};
Some(PickerItem::new((i + 1).to_string(), rel, detail))
})
.collect();
if items.is_empty() {
self.toast("harpoon: nothing pinned (use <leader>Ha to pin the active file)");
return;
}
self.open_picker(Picker::new(PickerKind::Harpoon, "Harpoon", items));
}
}