Skip to content

Commit 871584e

Browse files
authored
Merge pull request #306 from kyoheiu/feature/rename-multiple-items
Feature: rename multiple items
2 parents c35aeca + 0ad66fc commit 871584e

File tree

6 files changed

+158
-50
lines changed

6 files changed

+158
-50
lines changed

Cargo.lock

Lines changed: 21 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ zstd = "0.13.2"
3636
unicode-width = "0.2.0"
3737
git2 = {version = "0.19.0", default-features = false }
3838
normpath = "1.3.0"
39+
tempfile = "3.15.0"
3940

4041
[dev-dependencies]
4142
bwrap = { version = "1.3.0", features = ["use_std"] }

src/errors.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use std::path::PathBuf;
22

3-
#[derive(Debug)]
3+
#[derive(Debug, Default)]
44
pub enum FxError {
55
Arg(String),
66
TerminalSizeDetection,
@@ -22,6 +22,8 @@ pub enum FxError {
2222
Panic,
2323
#[cfg(any(target_os = "linux", target_os = "netbsd"))]
2424
Nix(String),
25+
#[default]
26+
Unknown,
2527
}
2628

2729
impl std::error::Error for FxError {}
@@ -51,6 +53,7 @@ impl std::fmt::Display for FxError {
5153
FxError::Panic => "Error: felix panicked".to_owned(),
5254
#[cfg(any(target_os = "linux", target_os = "netbsd"))]
5355
FxError::Nix(s) => s.to_owned(),
56+
FxError::Unknown => "Unknown error.".to_owned(),
5457
};
5558
write!(f, "{}", printable)
5659
}

src/op.rs

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ pub struct Operation {
1313
pub enum OpKind {
1414
Delete(DeletedFiles),
1515
Put(PutFiles),
16-
Rename(RenamedFile),
16+
Rename(Vec<(PathBuf, PathBuf)>),
1717
}
1818

1919
#[derive(Debug, Clone)]
@@ -30,12 +30,6 @@ pub struct PutFiles {
3030
pub dir: PathBuf,
3131
}
3232

33-
#[derive(Debug, Clone)]
34-
pub struct RenamedFile {
35-
pub original_name: PathBuf,
36-
pub new_name: PathBuf,
37-
}
38-
3933
impl Operation {
4034
/// Discard undone operations when new one is pushed.
4135
pub fn branch(&mut self) {
@@ -63,7 +57,14 @@ fn log(op: &OpKind) {
6357
info!("DELETE: {:?}", item_to_pathvec(&op.original));
6458
}
6559
OpKind::Rename(op) => {
66-
info!("RENAME: {:?} -> {:?}", op.original_name, op.new_name);
60+
if !op.is_empty() {
61+
info!(
62+
"RENAME: {:?}",
63+
op.iter()
64+
.map(|v| format!("{:?} -> {:?}", v.0, v.1))
65+
.collect::<Vec<String>>()
66+
);
67+
}
6768
}
6869
}
6970
}
@@ -84,8 +85,16 @@ pub fn relog(op: &OpKind, undo: bool) {
8485
info!("{} {:?}", result, item_to_pathvec(&op.original));
8586
}
8687
OpKind::Rename(op) => {
87-
result.push_str("RENAME");
88-
info!("{} {:?} -> {:?}", result, op.original_name, op.new_name);
88+
if !op.is_empty() {
89+
result.push_str("RENAME");
90+
info!(
91+
"{} {:?}",
92+
result,
93+
op.iter()
94+
.map(|v| format!("{:?} -> {:?}", v.0, v.1))
95+
.collect::<Vec<String>>()
96+
);
97+
}
8998
}
9099
}
91100
}

src/run.rs

Lines changed: 54 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -657,16 +657,19 @@ fn _run(mut state: State, session_path: PathBuf) -> Result<(), FxError> {
657657
}
658658
let mut dest: Option<PathBuf> = None;
659659
if let Ok(item) = state.get_item() {
660+
let mut err: Option<FxError> = None;
660661
match item.file_type {
661662
FileType::File => {
662663
execute!(screen, EnterAlternateScreen)?;
663664
if let Err(e) = state.open_file(item) {
664-
print_warning(e, state.layout.y);
665-
continue;
665+
err = Some(e);
666666
}
667667
execute!(screen, EnterAlternateScreen)?;
668668
hide_cursor();
669669
state.reload(state.layout.y)?;
670+
if let Some(e) = err {
671+
print_warning(e, state.layout.y);
672+
}
670673
continue;
671674
}
672675
FileType::Symlink => match &item.symlink_dir_path {
@@ -681,12 +684,14 @@ fn _run(mut state: State, session_path: PathBuf) -> Result<(), FxError> {
681684
None => {
682685
execute!(screen, EnterAlternateScreen)?;
683686
if let Err(e) = state.open_file(item) {
684-
print_warning(e, state.layout.y);
685-
continue;
687+
err = Some(e);
686688
}
687689
execute!(screen, EnterAlternateScreen)?;
688690
hide_cursor();
689-
state.redraw(state.layout.y);
691+
state.reload(state.layout.y)?;
692+
if let Some(e) = err {
693+
print_warning(e, state.layout.y);
694+
}
690695
continue;
691696
}
692697
},
@@ -1357,8 +1362,37 @@ fn _run(mut state: State, session_path: PathBuf) -> Result<(), FxError> {
13571362

13581363
//rename
13591364
KeyCode::Char('c') => {
1360-
//In visual mode, this is disabled.
1365+
//In visual mode, you can rename multiple items in default editor.
13611366
if state.v_start.is_some() {
1367+
let items: Vec<ItemBuffer> = state
1368+
.list
1369+
.iter()
1370+
.filter(|item| item.selected)
1371+
.map(ItemBuffer::new)
1372+
.collect();
1373+
execute!(screen, EnterAlternateScreen)?;
1374+
let result = state.rename_multiple_items(&items);
1375+
execute!(screen, EnterAlternateScreen)?;
1376+
hide_cursor();
1377+
state.reset_selection();
1378+
state.reload(state.layout.y)?;
1379+
match result {
1380+
Err(e) => {
1381+
print_warning(e, state.layout.y);
1382+
}
1383+
Ok(result_len) => {
1384+
let message = {
1385+
match result_len {
1386+
0 => "No item renamed.".to_owned(),
1387+
1 => "1 item renamed.".to_owned(),
1388+
count => {
1389+
format!("{} items renamed.", count)
1390+
}
1391+
}
1392+
};
1393+
print_info(message, state.layout.y);
1394+
}
1395+
}
13621396
continue;
13631397
}
13641398
if len == 0 {
@@ -1506,12 +1540,10 @@ fn _run(mut state: State, session_path: PathBuf) -> Result<(), FxError> {
15061540
}
15071541

15081542
state.operations.branch();
1509-
state.operations.push(OpKind::Rename(
1510-
RenamedFile {
1511-
original_name: item.file_path.clone(),
1512-
new_name: to,
1513-
},
1514-
));
1543+
state.operations.push(OpKind::Rename(vec![(
1544+
item.file_path.clone(),
1545+
to,
1546+
)]));
15151547

15161548
hide_cursor();
15171549
state.reload(state.layout.y)?;
@@ -2389,50 +2421,37 @@ fn _run(mut state: State, session_path: PathBuf) -> Result<(), FxError> {
23892421
}
23902422

23912423
//Execute command as is
2424+
let mut err: Option<&str> = None;
23922425
execute!(screen, EnterAlternateScreen)?;
23932426
if std::env::set_current_dir(&state.current_dir)
23942427
.is_err()
23952428
{
2396-
execute!(screen, EnterAlternateScreen)?;
2397-
print_warning(
2398-
"Cannot execute command",
2399-
state.layout.y,
2400-
);
2401-
break 'command;
2402-
}
2403-
if let Ok(sh) = std::env::var("SHELL") {
2429+
err =
2430+
Some("Changing current directory failed.");
2431+
} else if let Ok(sh) = std::env::var("SHELL") {
24042432
if std::process::Command::new(&sh)
24052433
.arg("-c")
2406-
.arg(&commands.join(" "))
2434+
.arg(commands.join(" "))
24072435
.status()
24082436
.is_err()
24092437
{
2410-
execute!(screen, EnterAlternateScreen)?;
2411-
state.redraw(state.layout.y);
2412-
print_warning(
2413-
"Cannot execute command",
2414-
state.layout.y,
2415-
);
2416-
break 'command;
2438+
err = Some("Command execution failed.");
24172439
}
24182440
} else if std::process::Command::new(command)
24192441
.args(&commands[1..])
24202442
.status()
24212443
.is_err()
24222444
{
2423-
execute!(screen, EnterAlternateScreen)?;
2424-
state.redraw(state.layout.y);
2425-
print_warning(
2426-
"Cannot execute command",
2427-
state.layout.y,
2428-
);
2429-
break 'command;
2445+
err = Some("Command execution failed.");
24302446
}
24312447

24322448
execute!(screen, EnterAlternateScreen)?;
24332449
hide_cursor();
24342450
info!("SHELL: {:?}", commands);
24352451
state.reload(state.layout.y)?;
2452+
if let Some(e) = err {
2453+
print_warning(e, state.layout.y);
2454+
}
24362455
break 'command;
24372456
}
24382457

src/state.rs

Lines changed: 59 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,8 @@ impl Registers {
190190
}
191191
}
192192

193-
/// To avoid cost copying ItemInfo, use ItemBuffer when tinkering with register.
193+
/// To avoid cost copying ItemInfo, use ItemBuffer
194+
/// when tinkering with register or multiple renaming.
194195
#[derive(Debug, Clone)]
195196
pub struct ItemBuffer {
196197
pub file_type: FileType,
@@ -505,7 +506,7 @@ impl State {
505506
let duration = duration_to_string(start.elapsed());
506507
let delete_message: String = {
507508
if total == 1 {
508-
format!("1 item deleted [{}]", duration)
509+
format!("1 item deleted. [{}]", duration)
509510
} else {
510511
let mut count = total.to_string();
511512
let _ = write!(count, " items deleted [{}]", duration);
@@ -921,7 +922,9 @@ impl State {
921922
pub fn undo(&mut self, op: &OpKind) -> Result<(), FxError> {
922923
match op {
923924
OpKind::Rename(op) => {
924-
std::fs::rename(&op.new_name, &op.original_name)?;
925+
for (original, new) in op {
926+
std::fs::rename(new, original)?;
927+
}
925928
self.operations.pos += 1;
926929
self.update_list()?;
927930
self.clear_and_show_headline();
@@ -959,7 +962,9 @@ impl State {
959962
pub fn redo(&mut self, op: &OpKind) -> Result<(), FxError> {
960963
match op {
961964
OpKind::Rename(op) => {
962-
std::fs::rename(&op.original_name, &op.new_name)?;
965+
for (original, new) in op {
966+
std::fs::rename(original, new)?;
967+
}
963968
self.operations.pos -= 1;
964969
self.update_list()?;
965970
self.clear_and_show_headline();
@@ -1287,6 +1292,56 @@ impl State {
12871292
self.list = result;
12881293
}
12891294

1295+
/// Rename selected items at once.
1296+
pub fn rename_multiple_items(&mut self, items: &[ItemBuffer]) -> Result<usize, FxError> {
1297+
let names: Vec<&str> = items.iter().map(|item| item.file_name.as_str()).collect();
1298+
let mut file = tempfile::NamedTempFile::new()?;
1299+
writeln!(file, "{}", names.join("\n"))?;
1300+
1301+
let mut default = Command::new(&self.default);
1302+
let path = file.into_temp_path();
1303+
if let Err(e) = default
1304+
.arg(&path)
1305+
.status()
1306+
.map_err(|_| FxError::DefaultEditor)
1307+
{
1308+
Err(e)
1309+
} else {
1310+
let new_names = fs::read_to_string(&path)?;
1311+
// clean up temp file
1312+
path.close()?;
1313+
let new_names: Vec<&str> = new_names
1314+
.split('\n')
1315+
.filter(|name| !name.is_empty())
1316+
.collect();
1317+
if new_names.len() != items.len() {
1318+
Err(FxError::Io(
1319+
format!(
1320+
"Rename failed: Expected {} names, but received {} names",
1321+
items.len(),
1322+
new_names.len()
1323+
)
1324+
.to_string(),
1325+
))
1326+
} else {
1327+
let mut result: Vec<(PathBuf, PathBuf)> = vec![];
1328+
for (i, new_name) in new_names.iter().enumerate() {
1329+
let mut to = self.current_dir.clone();
1330+
to.push(new_name);
1331+
if &items[i].file_name != new_name {
1332+
std::fs::rename(&items[i].file_path, &to)?;
1333+
result.push((items[i].file_path.clone(), to))
1334+
}
1335+
}
1336+
let len = result.len();
1337+
self.operations.branch();
1338+
self.operations.push(OpKind::Rename(result));
1339+
1340+
Ok(len)
1341+
}
1342+
}
1343+
}
1344+
12901345
/// Reset all item's selected state and exit the select mode.
12911346
pub fn reset_selection(&mut self) {
12921347
for item in self.list.iter_mut() {

0 commit comments

Comments
 (0)