Skip to content

Commit e7f90cf

Browse files
committed
[ed] updates
Completed Features Phase 4.1: Escaped newline in substitute replacement (line splitting) - Modified convert_replacement() to preserve \<newline> sequences - Modified execute_substitute() to split lines when replacement contains internal newlines - Added line number tracking to handle multiple line splits in a range - Added guard to prevent line splitting in global command context (POSIX requirement) - Files modified: editors/ed/editor.rs, editors/ed/buffer.rs Phase 3.1: Multi-line command lists for g/v - Added check_line_continuation() function to detect backslash-newline continuation - Modified process_line() to accumulate continued lines before parsing - This enables both substitute replacement continuation and g/v command list continuation - Files modified: editors/ed/editor.rs Phase 3.3 & 3.4: Interactive G/V commands - Implemented execute_global_interactive() function - For each matching line: prints the line, reads a command from user - Handles empty line (skip), & (repeat previous command), and regular commands - Tracks line number shifts when lines are deleted/added - Checks for SIGINT to abort - Added last_interactive_cmd field to track previous command for & - Files modified: editors/ed/editor.rs
1 parent 1305006 commit e7f90cf

File tree

3 files changed

+403
-15
lines changed

3 files changed

+403
-15
lines changed

editors/ed/buffer.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,11 @@ impl Buffer {
116116
self.in_global = false;
117117
}
118118

119+
/// Check if currently in a global command context.
120+
pub fn is_in_global(&self) -> bool {
121+
self.in_global
122+
}
123+
119124
/// Undo the last change. Returns true if undo was performed.
120125
pub fn undo(&mut self) -> bool {
121126
if let Some(record) = self.undo_record.take() {

editors/ed/editor.rs

Lines changed: 252 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ pub struct Editor<R: BufRead, W: Write> {
5454
pub should_quit: bool,
5555
/// Quit warning given (for modified buffer)
5656
quit_warning_given: bool,
57+
/// Previous command for & in interactive global (G/V)
58+
last_interactive_cmd: Option<String>,
5759
}
5860

5961
impl<R: BufRead, W: Write> Editor<R, W> {
@@ -78,6 +80,7 @@ impl<R: BufRead, W: Write> Editor<R, W> {
7880
pending_command: None,
7981
should_quit: false,
8082
quit_warning_given: false,
83+
last_interactive_cmd: None,
8184
}
8285
}
8386

@@ -308,14 +311,57 @@ impl<R: BufRead, W: Write> Editor<R, W> {
308311
}
309312
}
310313

314+
/// Check if a line has a continuation (ends with unescaped backslash).
315+
/// Returns (trimmed_line_without_backslash, needs_continuation).
316+
fn check_line_continuation(line: &str) -> (String, bool) {
317+
let trimmed = line.trim_end_matches('\n');
318+
319+
// Count trailing backslashes
320+
let mut backslash_count = 0;
321+
for ch in trimmed.chars().rev() {
322+
if ch == '\\' {
323+
backslash_count += 1;
324+
} else {
325+
break;
326+
}
327+
}
328+
329+
// Odd number of trailing backslashes = continuation
330+
if backslash_count > 0 && backslash_count % 2 == 1 {
331+
// Remove the trailing backslash and indicate continuation needed
332+
let without_backslash = &trimmed[..trimmed.len() - 1];
333+
(without_backslash.to_string(), true)
334+
} else {
335+
(trimmed.to_string(), false)
336+
}
337+
}
338+
311339
/// Process a line of input.
312340
pub fn process_line(&mut self, line: &str) -> io::Result<bool> {
313341
if self.in_input_mode {
314342
return self.process_input_line(line);
315343
}
316344

317-
let trimmed = line.trim_end();
318-
match parse(trimmed) {
345+
// Handle line continuation (backslash-newline)
346+
let (first_part, needs_continuation) = Self::check_line_continuation(line);
347+
let full_line = if needs_continuation {
348+
let mut accumulated = first_part;
349+
// Keep reading lines until we get one without continuation
350+
while let Some(next_line) = self.read_line()? {
351+
let (part, more) = Self::check_line_continuation(&next_line);
352+
// Join with embedded newline (the backslash-newline becomes \n in content)
353+
accumulated.push('\n');
354+
accumulated.push_str(&part);
355+
if !more {
356+
break;
357+
}
358+
}
359+
accumulated
360+
} else {
361+
first_part
362+
};
363+
364+
match parse(&full_line) {
319365
Ok(cmd) => {
320366
if let Err(e) = self.execute_command(cmd) {
321367
self.print_error(&e)?;
@@ -432,11 +478,10 @@ impl<R: BufRead, W: Write> Editor<R, W> {
432478
self.execute_global(addr1, addr2, &pattern, &commands, true)
433479
}
434480
Command::GlobalInteractive(addr1, addr2, pattern) => {
435-
// For now, treat as non-interactive with 'p' command
436-
self.execute_global(addr1, addr2, &pattern, "p", false)
481+
self.execute_global_interactive(addr1, addr2, &pattern, false)
437482
}
438483
Command::GlobalNotInteractive(addr1, addr2, pattern) => {
439-
self.execute_global(addr1, addr2, &pattern, "p", true)
484+
self.execute_global_interactive(addr1, addr2, &pattern, true)
440485
}
441486
Command::Help => {
442487
if let Some(ref err) = self.last_error {
@@ -747,6 +792,7 @@ impl<R: BufRead, W: Write> Editor<R, W> {
747792

748793
/// Convert POSIX ed replacement string to regex crate format.
749794
/// POSIX: & -> matched string, \1-\9 -> back-references, \& -> literal &
795+
/// POSIX: \<newline> -> split line at this point
750796
/// Regex crate: $0 -> matched string, $1-$9 -> back-references, $$ -> literal $
751797
fn convert_replacement(&self, repl: &str) -> String {
752798
let mut result = String::new();
@@ -769,6 +815,12 @@ impl<R: BufRead, W: Write> Editor<R, W> {
769815
result.push('\\');
770816
chars.next();
771817
}
818+
'\n' => {
819+
// POSIX: \<newline> causes line split
820+
// Preserve the newline in replacement
821+
result.push('\n');
822+
chars.next();
823+
}
772824
_ => {
773825
result.push('\\');
774826
}
@@ -850,9 +902,14 @@ impl<R: BufRead, W: Write> Editor<R, W> {
850902

851903
let mut any_match = false;
852904
let mut last_matched_line = start;
905+
// Track offset when lines are split (inserted lines shift subsequent line numbers)
906+
let mut offset: usize = 0;
853907

854908
for i in start..=end {
855-
if let Some(line) = self.buf.get_line(i) {
909+
// Compute actual buffer position accounting for previously inserted lines
910+
let actual_line = i + offset;
911+
912+
if let Some(line) = self.buf.get_line(actual_line) {
856913
let line_content = line.clone();
857914
let new_line = if global {
858915
re.replace_all(&line_content, regex_repl.as_str())
@@ -874,14 +931,38 @@ impl<R: BufRead, W: Write> Editor<R, W> {
874931

875932
if new_line != line_content {
876933
any_match = true;
877-
last_matched_line = i;
878-
// Ensure line ends with newline
879-
let final_line = if new_line.ends_with('\n') {
880-
new_line
934+
935+
// Check for internal newlines (line splitting)
936+
// Strip trailing newline first, then check for remaining newlines
937+
let content = new_line.trim_end_matches('\n');
938+
if content.contains('\n') {
939+
// POSIX: Line splitting via \<newline> is not allowed in g/v commands
940+
if self.buf.is_in_global() {
941+
return Err(EdError::Generic(
942+
"cannot split lines in global command".to_string(),
943+
));
944+
}
945+
946+
// Split into multiple lines, each ending with newline
947+
let lines: Vec<String> =
948+
content.split('\n').map(|s| format!("{}\n", s)).collect();
949+
950+
let num_new_lines = lines.len();
951+
self.buf.change(actual_line, actual_line, &lines)?;
952+
// Update last_matched_line to point to the last inserted line
953+
last_matched_line = actual_line + num_new_lines - 1;
954+
// Track extra lines inserted (we replaced 1 line with num_new_lines)
955+
offset += num_new_lines - 1;
881956
} else {
882-
format!("{}\n", new_line)
883-
};
884-
self.buf.change(i, i, &[final_line])?;
957+
// No line splitting, just replace the single line
958+
let final_line = if new_line.ends_with('\n') {
959+
new_line
960+
} else {
961+
format!("{}\n", new_line)
962+
};
963+
self.buf.change(actual_line, actual_line, &[final_line])?;
964+
last_matched_line = actual_line;
965+
}
885966
}
886967
}
887968
}
@@ -1108,10 +1189,20 @@ impl<R: BufRead, W: Write> Editor<R, W> {
11081189
}
11091190
cmd if !cmd.is_empty() => {
11101191
// Try to parse and execute other commands
1111-
if let Ok(parsed_cmd) = parse(cmd) {
1112-
if self.execute_command(parsed_cmd).is_ok() {
1192+
match parse(cmd) {
1193+
Ok(parsed_cmd) => {
1194+
if let Err(e) = self.execute_command(parsed_cmd) {
1195+
// Abort global on error and propagate
1196+
self.buf.end_global();
1197+
return Err(e);
1198+
}
11131199
last_successful_line = self.buf.cur_line;
11141200
}
1201+
Err(e) => {
1202+
// Abort global on parse error
1203+
self.buf.end_global();
1204+
return Err(e);
1205+
}
11151206
}
11161207
}
11171208
_ => {}
@@ -1127,6 +1218,152 @@ impl<R: BufRead, W: Write> Editor<R, W> {
11271218
Ok(())
11281219
}
11291220

1221+
/// Execute an interactive global command (G or V).
1222+
/// For each matching line: print it, read a command from user, execute.
1223+
fn execute_global_interactive(
1224+
&mut self,
1225+
addr1: Address,
1226+
addr2: Address,
1227+
pattern: &str,
1228+
invert: bool,
1229+
) -> EdResult<()> {
1230+
let (start, end) = self.resolve_range(&addr1, &addr2)?;
1231+
1232+
// Use previous pattern if empty
1233+
let pat = if pattern.is_empty() {
1234+
self.last_pattern
1235+
.as_ref()
1236+
.ok_or(EdError::NoPreviousPattern)?
1237+
.clone()
1238+
} else {
1239+
pattern.to_string()
1240+
};
1241+
1242+
self.last_pattern = Some(pat.clone());
1243+
1244+
let re = Regex::new(&pat).map_err(|e| EdError::Syntax(e.to_string()))?;
1245+
1246+
// Collect matching lines first
1247+
let mut matching_lines = Vec::new();
1248+
for i in start..=end {
1249+
if let Some(line) = self.buf.get_line(i) {
1250+
let matches = re.is_match(line);
1251+
if matches != invert {
1252+
matching_lines.push(i);
1253+
}
1254+
}
1255+
}
1256+
1257+
if matching_lines.is_empty() {
1258+
return Ok(());
1259+
}
1260+
1261+
// Save one undo record for the entire operation
1262+
self.buf.begin_global();
1263+
1264+
let mut idx = 0;
1265+
while idx < matching_lines.len() {
1266+
// Check for SIGINT
1267+
if crate::SIGINT_RECEIVED.swap(false, Ordering::SeqCst) {
1268+
self.buf.end_global();
1269+
writeln!(self.writer, "?")?;
1270+
self.last_error = Some("Interrupt".to_string());
1271+
return Ok(());
1272+
}
1273+
1274+
let line_num = matching_lines[idx];
1275+
1276+
// Check if line still exists (may have been deleted by previous commands)
1277+
if line_num == 0 || line_num > self.buf.line_count() {
1278+
idx += 1;
1279+
continue;
1280+
}
1281+
1282+
// Print the line
1283+
if let Some(line) = self.buf.get_line(line_num) {
1284+
write!(self.writer, "{}", line)?;
1285+
}
1286+
self.writer.flush()?;
1287+
1288+
// Read command from user
1289+
let cmd_line = match self.read_line()? {
1290+
Some(l) => l,
1291+
None => break, // EOF
1292+
};
1293+
1294+
let trimmed = cmd_line.trim_end_matches('\n');
1295+
1296+
// Empty line = skip, no action
1297+
if trimmed.is_empty() {
1298+
idx += 1;
1299+
continue;
1300+
}
1301+
1302+
// & = repeat previous command
1303+
let cmd_str = if trimmed == "&" {
1304+
match &self.last_interactive_cmd {
1305+
Some(prev) => prev.clone(),
1306+
None => {
1307+
// No previous command
1308+
idx += 1;
1309+
continue;
1310+
}
1311+
}
1312+
} else {
1313+
self.last_interactive_cmd = Some(trimmed.to_string());
1314+
trimmed.to_string()
1315+
};
1316+
1317+
// Set current line and execute
1318+
if self.buf.set_cur_line(line_num).is_err() {
1319+
idx += 1;
1320+
continue;
1321+
}
1322+
1323+
// Track line count before and after to adjust remaining line numbers
1324+
let lines_before = self.buf.line_count();
1325+
1326+
match parse(&cmd_str) {
1327+
Ok(cmd) => {
1328+
// Check for forbidden commands
1329+
if matches!(
1330+
cmd,
1331+
Command::Global(..)
1332+
| Command::GlobalNot(..)
1333+
| Command::GlobalInteractive(..)
1334+
| Command::GlobalNotInteractive(..)
1335+
| Command::Shell(..)
1336+
) {
1337+
writeln!(self.writer, "?")?;
1338+
self.last_error = Some("invalid command".to_string());
1339+
} else if let Err(e) = self.execute_command(cmd) {
1340+
self.print_error(&e)?;
1341+
}
1342+
}
1343+
Err(e) => {
1344+
self.print_error(&e)?;
1345+
}
1346+
}
1347+
1348+
// Adjust remaining line numbers if lines were deleted or added
1349+
let lines_after = self.buf.line_count();
1350+
if lines_after != lines_before {
1351+
let diff = lines_after as isize - lines_before as isize;
1352+
// Adjust all remaining line numbers (those after current position)
1353+
for remaining_line in &mut matching_lines[(idx + 1)..] {
1354+
if *remaining_line > line_num {
1355+
*remaining_line = (*remaining_line as isize + diff) as usize;
1356+
}
1357+
}
1358+
}
1359+
1360+
idx += 1;
1361+
}
1362+
1363+
self.buf.end_global();
1364+
Ok(())
1365+
}
1366+
11301367
/// Check for and handle SIGINT signal.
11311368
/// Returns true if SIGINT was received and handled.
11321369
fn check_sigint(&mut self) -> io::Result<bool> {

0 commit comments

Comments
 (0)