Skip to content

Commit 2bb1181

Browse files
committed
Handle spaces in filenames (on unix)
1 parent e155ac3 commit 2bb1181

File tree

1 file changed

+75
-4
lines changed

1 file changed

+75
-4
lines changed

crates/modalkit/src/editing/completion.rs

Lines changed: 75 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -352,10 +352,73 @@ impl LineCompleter {
352352
}
353353
}
354354

355+
mod parse {
356+
use std::borrow::Cow;
357+
358+
use nom::{
359+
branch::alt,
360+
bytes::complete::{escaped_transform, is_not, tag},
361+
character::complete::anychar,
362+
combinator::{eof, opt, value},
363+
IResult,
364+
};
365+
366+
use crate::editing::{cursor::Cursor, rope::EditRope};
367+
368+
fn filepath_char(input: &str) -> IResult<&str, &str> {
369+
is_not("\t\n\\ |\"")(input)
370+
}
371+
372+
fn parse_filepath(input: &str) -> IResult<&str, String> {
373+
escaped_transform(
374+
filepath_char,
375+
'\\',
376+
alt((
377+
value("\\", tag("\\")),
378+
value(" ", tag(" ")),
379+
value("#", tag("#")),
380+
value("%", tag("%")),
381+
value("|", tag("|")),
382+
value("\"", tag("\"")),
383+
)),
384+
)(input)
385+
}
386+
387+
fn parse_filepath_end(input: &str) -> IResult<&str, String> {
388+
let (input, res) = parse_filepath(input)?;
389+
let (input, _) = eof(input)?;
390+
391+
Ok((input, res))
392+
}
393+
394+
fn filepath_at_end(mut input: &str) -> IResult<&str, String> {
395+
loop {
396+
let (i, res) = opt(parse_filepath_end)(input)?;
397+
if let Some(res) = res {
398+
return Ok((i, res));
399+
}
400+
while let Ok((i, _)) = filepath_char(input) {
401+
input = i;
402+
}
403+
input = anychar(i)?.0;
404+
}
405+
}
406+
407+
pub fn filepath_suffix(input: &EditRope, cursor: &Cursor) -> Option<String> {
408+
let mut start = cursor.clone();
409+
start.left(4096); // no good way to access NAME_MAX in rust
410+
let prefix = input.slice(input.cursor_to_offset(&start)..input.cursor_to_offset(cursor));
411+
let prefix = Cow::from(&prefix);
412+
413+
filepath_at_end(prefix.as_ref()).ok().map(|(_, path)| path)
414+
}
415+
}
416+
355417
/// Complete filenames within a path leading up to the cursor.
356418
pub fn complete_path(input: &EditRope, cursor: &mut Cursor) -> Vec<String> {
357-
let filepath = input.get_prefix_word(cursor, &WordStyle::FilePath);
358-
let filepath = filepath.unwrap_or_else(EditRope::empty);
419+
let Some(filepath) = parse::filepath_suffix(input, cursor) else {
420+
return vec![];
421+
};
359422

360423
let filepath = Cow::from(&filepath);
361424
let Ok(filepath_unexpanded) = shellexpand::env(filepath.as_ref()) else {
@@ -402,7 +465,7 @@ pub fn complete_path(input: &EditRope, cursor: &mut Cursor) -> Vec<String> {
402465

403466
// The .parent() and .file_name() methods treat . especially, so we
404467
// have to special-case completion of hidden files here.
405-
let _ = input.get_prefix_word_mut(cursor, &WordStyle::FileName);
468+
let _ = input.get_prefix_word_mut(cursor, &WordStyle::CharSet(|c| c != MAIN_SEPARATOR)); // TODO: fix for windows
406469

407470
if let Ok(dir) = path.read_dir() {
408471
let filter = |entry: DirEntry| {
@@ -429,7 +492,7 @@ pub fn complete_path(input: &EditRope, cursor: &mut Cursor) -> Vec<String> {
429492
} else {
430493
// complete a path
431494

432-
let _ = input.get_prefix_word_mut(cursor, &WordStyle::FileName);
495+
let _ = input.get_prefix_word_mut(cursor, &WordStyle::CharSet(|c| c != MAIN_SEPARATOR)); // TODO: fix for windows
433496

434497
let Some(prefix) = path.components().next_back() else {
435498
return vec![];
@@ -497,6 +560,14 @@ pub fn complete_path(input: &EditRope, cursor: &mut Cursor) -> Vec<String> {
497560
a.cmp(b)
498561
});
499562

563+
for comp in &mut res {
564+
for c in ["\\\\", "\\ ", "\\#", "\\%", "\\|", "\\\""] {
565+
if comp.contains(&c[1..2]) {
566+
*comp = comp.replace(&c[1..2], c);
567+
}
568+
}
569+
}
570+
500571
return res;
501572
}
502573

0 commit comments

Comments
 (0)