1use std::cmp::Ordering;
7use std::collections::{BTreeSet, HashSet};
8use std::ops::Range;
9
10use serde::{Deserialize, Serialize};
11
12use crate::changeset::{Bias, ChangeSet, Edit};
13use crate::history::EditKind;
14use unicode_segmentation::UnicodeSegmentation;
15
16use crate::position::{ByteOffset, grapheme_width, visual_col_to_byte, visual_col_to_byte_ceil};
17use crate::resolve::Resolved;
18use crate::selection::{Selection, SelectionSet};
19use crate::text::Document;
20
21#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
22pub enum CaseMode {
23 Upper,
24 Lower,
25 Title,
27 TitleBlend,
30 Sentence,
32 SentenceBlend,
34 Invert,
36 Random {
39 seed: u64,
40 },
41}
42
43#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
44pub struct SortOptions {
45 pub descending: bool,
46 pub case_insensitive: bool,
47 pub numeric: bool,
50 pub remove_duplicates: bool,
52 #[serde(default)]
54 pub by_length: bool,
55}
56
57pub fn convert_case(text: &str, mode: CaseMode) -> String {
60 match mode {
61 CaseMode::Upper => text.to_uppercase(),
62 CaseMode::Lower => text.to_lowercase(),
63 CaseMode::Invert => text
64 .chars()
65 .flat_map(|c| -> Box<dyn Iterator<Item = char>> {
66 if c.is_uppercase() {
67 Box::new(c.to_lowercase())
68 } else if c.is_lowercase() {
69 Box::new(c.to_uppercase())
70 } else {
71 Box::new(std::iter::once(c))
72 }
73 })
74 .collect(),
75 CaseMode::Title | CaseMode::TitleBlend => {
76 let blend = mode == CaseMode::TitleBlend;
77 let mut out = String::with_capacity(text.len());
78 let mut in_word = false;
79 for c in text.chars() {
80 if c.is_alphanumeric() {
81 if !in_word {
82 out.extend(c.to_uppercase());
83 } else if blend {
84 out.push(c);
85 } else {
86 out.extend(c.to_lowercase());
87 }
88 in_word = true;
89 } else {
90 out.push(c);
91 in_word = in_word && (c == '\'' || c == '\u{2019}');
93 }
94 }
95 out
96 }
97 CaseMode::Sentence | CaseMode::SentenceBlend => {
98 let blend = mode == CaseMode::SentenceBlend;
102 let chars: Vec<char> = text.chars().collect();
103 let mut out = String::with_capacity(text.len());
104 let (mut starts, mut after_break) = (true, false);
105 for (i, &c) in chars.iter().enumerate() {
106 if c.is_alphabetic() {
107 if starts {
108 out.extend(c.to_uppercase());
109 } else if blend {
110 out.push(c);
111 } else {
112 out.extend(c.to_lowercase());
113 }
114 starts = false;
115 after_break = false;
116 continue;
117 }
118 out.push(c);
119 if c == '\n' {
120 starts |= after_break;
121 after_break = true;
122 } else if !c.is_whitespace() {
123 after_break = false;
125 if matches!(c, '.' | '!' | '?') {
126 starts |= !chars.get(i + 1).is_some_and(|n| n.is_alphanumeric());
127 }
128 }
129 }
130 out
131 }
132 CaseMode::Random { seed } => {
133 let mut state = seed | 1;
135 let mut upper = || {
136 state ^= state >> 12;
137 state ^= state << 25;
138 state ^= state >> 27;
139 state.wrapping_mul(0x2545_F491_4F6C_DD1D) >> 63 == 1
140 };
141 text.chars()
142 .flat_map(|c| -> Box<dyn Iterator<Item = char>> {
143 if !c.is_alphabetic() {
144 Box::new(std::iter::once(c))
145 } else if upper() {
146 Box::new(c.to_uppercase())
147 } else {
148 Box::new(c.to_lowercase())
149 }
150 })
151 .collect()
152 }
153 }
154}
155
156fn line_range(doc: &Document, line: usize) -> Range<ByteOffset> {
157 doc.line_start(line)..doc.line_end(line)
158}
159
160fn lines_of(doc: &Document, s: &Selection) -> (usize, usize) {
163 let first = doc.line_of(s.start());
164 let mut last = doc.line_of(s.end());
165 if !s.is_empty() && last > first && s.end() == doc.line_start(last) {
166 last -= 1;
167 }
168 (first, last)
169}
170
171fn merge(mut ranges: Vec<Range<ByteOffset>>) -> Vec<Range<ByteOffset>> {
173 ranges.sort_by_key(|r| (r.start, r.end));
174 let mut out: Vec<Range<ByteOffset>> = Vec::with_capacity(ranges.len());
175 for r in ranges {
176 match out.last_mut() {
177 Some(last) if r.start <= last.end => last.end = last.end.max(r.end),
178 _ => out.push(r),
179 }
180 }
181 out
182}
183
184fn edit_result(changes: ChangeSet, after: SelectionSet, sels: &SelectionSet) -> Resolved {
185 if changes.is_empty() {
186 Resolved::Selections(sels.clone())
187 } else {
188 Resolved::Edit {
189 changes,
190 after,
191 kind: EditKind::Other,
192 }
193 }
194}
195
196fn map_before(sels: &SelectionSet, changes: &ChangeSet) -> SelectionSet {
199 sels.map(|s| Selection {
200 anchor: changes.map_position(s.anchor, Bias::Before),
201 head: changes.map_position(s.head, Bias::Before),
202 goal: None,
203 })
204}
205
206pub(crate) fn case(doc: &Document, sels: &SelectionSet, mode: CaseMode) -> Resolved {
208 let targets = sels
209 .ranges()
210 .iter()
211 .map(|s| {
212 if s.is_empty() {
213 line_range(doc, doc.line_of(s.head))
214 } else {
215 s.range()
216 }
217 })
218 .collect();
219 let pieces: Vec<(Range<ByteOffset>, String, String)> = merge(targets)
220 .into_iter()
221 .map(|r| {
222 let old = doc.slice(r.clone());
223 let new = convert_case(&old, mode);
224 (r, old, new)
225 })
226 .collect();
227 let changes = ChangeSet::new(
228 doc.len(),
229 pieces
230 .iter()
231 .filter(|(_, old, new)| old != new)
232 .map(|(r, _, new)| Edit::new(r.start, r.end, new.as_str()))
233 .collect(),
234 );
235 let remap = |pos: ByteOffset| {
238 let Some((r, old, new)) = pieces.iter().find(|(r, ..)| r.start <= pos && pos <= r.end)
239 else {
240 return changes.map_position(pos, Bias::Before);
241 };
242 let new_start = changes.map_position(r.start, Bias::Before);
243 if pos == r.end {
244 return ByteOffset(new_start.0 + new.len());
245 }
246 let chars = old[..pos.0 - r.start.0].chars().count();
247 let off = new.char_indices().nth(chars).map_or(new.len(), |(i, _)| i);
248 ByteOffset(new_start.0 + off)
249 };
250 let after = sels
251 .map(|s| Selection::new(remap(s.anchor), remap(s.head)))
252 .with_rect(sels.rect().cloned());
253 edit_result(changes, after, sels)
254}
255
256fn leading_number(s: &str) -> Option<f64> {
258 let t = s.trim_start();
259 let b = t.as_bytes();
260 let mut i = 0;
261 if i < b.len() && (b[i] == b'-' || b[i] == b'+') {
262 i += 1;
263 }
264 let digits_start = i;
265 while i < b.len() && b[i].is_ascii_digit() {
266 i += 1;
267 }
268 let mut digits = i - digits_start;
269 if i < b.len() && b[i] == b'.' {
270 let frac_start = i + 1;
271 let mut j = frac_start;
272 while j < b.len() && b[j].is_ascii_digit() {
273 j += 1;
274 }
275 if j > frac_start {
276 digits += j - frac_start;
277 i = j;
278 }
279 }
280 if digits == 0 {
281 return None;
282 }
283 t[..i].parse().ok()
284}
285
286pub(crate) fn sort(
289 doc: &Document,
290 sels: &SelectionSet,
291 o: SortOptions,
292 tab_width: usize,
293) -> Resolved {
294 reorder(doc, sels, tab_width, o.case_insensitive, |lines, keys| {
295 let compare = |a: usize, b: usize| -> Ordering {
296 let ord = if o.numeric {
297 match (leading_number(&keys[a]), leading_number(&keys[b])) {
298 (Some(x), Some(y)) => x.total_cmp(&y),
299 (None, Some(_)) => Ordering::Less,
300 (Some(_), None) => Ordering::Greater,
301 (None, None) => keys[a].cmp(&keys[b]),
302 }
303 } else if o.by_length {
304 let len = |i: usize| keys[i].chars().count();
305 len(a).cmp(&len(b)).then_with(|| keys[a].cmp(&keys[b]))
306 } else {
307 keys[a].cmp(&keys[b])
308 };
309 if o.descending { ord.reverse() } else { ord }
310 };
311 let mut order: Vec<usize> = (0..lines.len()).collect();
312 order.sort_by(|&a, &b| compare(a, b)); if o.remove_duplicates {
314 let same = |a: &str, b: &str| {
315 if o.case_insensitive {
316 a.to_lowercase() == b.to_lowercase()
317 } else {
318 a == b
319 }
320 };
321 order.dedup_by(|b, a| same(&lines[*a], &lines[*b]));
322 }
323 order
324 })
325}
326
327pub(crate) fn shuffle(
330 doc: &Document,
331 sels: &SelectionSet,
332 seed: u64,
333 tab_width: usize,
334) -> Resolved {
335 reorder(doc, sels, tab_width, false, |lines, _| {
336 let mut state = seed | 1;
338 let mut next = || {
339 state ^= state >> 12;
340 state ^= state << 25;
341 state ^= state >> 27;
342 state.wrapping_mul(0x2545_F491_4F6C_DD1D)
343 };
344 let mut order: Vec<usize> = (0..lines.len()).collect();
345 for i in (1..order.len()).rev() {
346 let j = (next() % (i as u64 + 1)) as usize;
347 order.swap(i, j);
348 }
349 order
350 })
351}
352
353fn reorder(
357 doc: &Document,
358 sels: &SelectionSet,
359 tab_width: usize,
360 case_insensitive: bool,
361 order: impl FnOnce(&[String], &[String]) -> Vec<usize>,
362) -> Resolved {
363 let rect = sels.rect().cloned();
364 let any_selected = sels.ranges().iter().any(|s| !s.is_empty());
365 let (first, mut last) = if let Some(r) = &rect {
366 (r.top(), r.bottom())
367 } else if any_selected {
368 let spans: Vec<_> = sels.ranges().iter().map(|s| lines_of(doc, s)).collect();
369 let first = spans.iter().map(|s| s.0).min().unwrap_or(0);
370 let last = spans.iter().map(|s| s.1).max().unwrap_or(0);
371 (first, last)
372 } else {
373 (0, doc.line_count() - 1)
374 };
375 if last > first && last + 1 == doc.line_count() && doc.line_text(last).is_empty() {
377 last -= 1;
378 }
379 if last <= first {
380 return Resolved::Selections(sels.clone());
381 }
382 let lines: Vec<String> = (first..=last).map(|l| doc.line_text(l)).collect();
383 let keys: Vec<String> = (first..=last)
384 .zip(&lines)
385 .map(|(line, text)| {
386 let key = match &rect {
387 Some(r) => {
388 let start = doc.line_start(line).0;
389 let right = visual_col_to_byte(doc, line, r.right(), tab_width).0 - start;
390 let left = (visual_col_to_byte_ceil(doc, line, r.left(), tab_width).0 - start)
391 .min(right);
392 text[left..right].to_owned()
393 }
394 None => text.clone(),
395 };
396 if case_insensitive {
397 key.to_lowercase()
398 } else {
399 key
400 }
401 })
402 .collect();
403 let order = order(&lines, &keys);
404 let new: Vec<&str> = order.iter().map(|&i| lines[i].as_str()).collect();
405 let new = new.join("\n");
406 let start = doc.line_start(first);
407 let end = doc.line_end(last);
408 let changes = ChangeSet::new(doc.len(), vec![Edit::new(start, end, new.as_str())]);
409 let after = if any_selected || rect.is_some() {
410 SelectionSet::single(Selection::new(start, ByteOffset(start.0 + new.len())))
411 } else {
412 SelectionSet::caret(start)
413 };
414 edit_result(changes, after, sels)
415}
416
417fn selected_lines(doc: &Document, sels: &SelectionSet) -> BTreeSet<usize> {
419 let selected: BTreeSet<usize> = sels
420 .ranges()
421 .iter()
422 .filter(|s| !s.is_empty())
423 .flat_map(|s| {
424 let (a, b) = lines_of(doc, s);
425 a..=b
426 })
427 .collect();
428 if selected.is_empty() {
429 (0..doc.line_count()).collect()
430 } else {
431 selected
432 }
433}
434
435pub(crate) fn trim_trailing(doc: &Document, sels: &SelectionSet) -> Resolved {
438 let edits = selected_lines(doc, sels)
439 .into_iter()
440 .filter_map(|line| {
441 let text = doc.line_text(line);
442 let keep = text.trim_end_matches([' ', '\t']).len();
443 (keep < text.len()).then(|| {
444 let start = doc.line_start(line).0;
445 Edit::new(ByteOffset(start + keep), doc.line_end(line), "")
446 })
447 })
448 .collect();
449 let changes = ChangeSet::new(doc.len(), edits);
450 let after = map_before(sels, &changes);
451 edit_result(changes, after, sels)
452}
453
454pub(crate) fn duplicate(doc: &Document, sels: &SelectionSet) -> Resolved {
457 let mut lines = BTreeSet::new();
458 let mut edits = Vec::new();
459 for s in sels.ranges() {
460 if s.is_empty() {
461 lines.insert(doc.line_of(s.head));
462 } else {
463 edits.push(Edit::new(s.end(), s.end(), doc.slice(s.range())));
464 }
465 }
466 for line in lines {
467 let end = doc.line_end(line);
468 edits.push(Edit::new(end, end, format!("\n{}", doc.line_text(line))));
469 }
470 let changes = ChangeSet::new(doc.len(), edits);
471 let after = map_before(sels, &changes);
472 edit_result(changes, after, sels)
473}
474
475pub(crate) fn move_lines(doc: &Document, sels: &SelectionSet, up: bool) -> Resolved {
478 let spans: Vec<_> = sels.ranges().iter().map(|s| lines_of(doc, s)).collect();
479 let first = spans.iter().map(|s| s.0).min().unwrap_or(0);
480 let last = spans.iter().map(|s| s.1).max().unwrap_or(0);
481 let block = doc.slice(doc.line_start(first)..doc.line_end(last));
482 let (edit, shift): (Edit, isize) = if up {
483 if first == 0 {
484 return Resolved::Selections(sels.clone());
485 }
486 let prev = doc.line_text(first - 1);
487 (
488 Edit::new(
489 doc.line_start(first - 1),
490 doc.line_end(last),
491 format!("{block}\n{prev}"),
492 ),
493 -(prev.len() as isize + 1),
494 )
495 } else {
496 if last + 1 >= doc.line_count() {
497 return Resolved::Selections(sels.clone());
498 }
499 let next = doc.line_text(last + 1);
500 (
501 Edit::new(
502 doc.line_start(first),
503 doc.line_end(last + 1),
504 format!("{next}\n{block}"),
505 ),
506 next.len() as isize + 1,
507 )
508 };
509 let changes = ChangeSet::new(doc.len(), vec![edit]);
510 let new_len = changes.new_len().0 as isize;
515 let shift_pos = |p: ByteOffset| ByteOffset((p.0 as isize + shift).clamp(0, new_len) as usize);
516 let after = sels.map(|s| Selection::new(shift_pos(s.anchor), shift_pos(s.head)));
517 edit_result(changes, after, sels)
518}
519
520pub(crate) fn join(doc: &Document, sels: &SelectionSet) -> Resolved {
523 let last_line = doc.line_count() - 1;
524 let mut breaks = BTreeSet::new();
525 for s in sels.ranges() {
526 let (a, b) = lines_of(doc, s);
527 let b = if a == b { (a + 1).min(last_line) } else { b };
528 breaks.extend(a..b);
529 }
530 let edits = breaks
531 .into_iter()
532 .map(|line| {
533 let text = doc.line_text(line);
534 let next = doc.line_text(line + 1);
535 let kept = text.trim_end_matches([' ', '\t']);
536 let rest = next.trim_start_matches([' ', '\t']);
537 let start = ByteOffset(doc.line_start(line).0 + kept.len());
538 let end = ByteOffset(doc.line_start(line + 1).0 + (next.len() - rest.len()));
539 let sep = if kept.is_empty() || rest.is_empty() {
540 ""
541 } else {
542 " "
543 };
544 Edit::new(start, end, sep)
545 })
546 .collect();
547 let changes = ChangeSet::new(doc.len(), edits);
548 let after = map_before(sels, &changes);
549 edit_result(changes, after, sels)
550}
551
552pub(crate) fn split_lines(
560 doc: &Document,
561 sels: &SelectionSet,
562 width: usize,
563 tab_width: usize,
564) -> Resolved {
565 let mut edits = Vec::new();
566 for line in touched_lines(doc, sels) {
567 let text = doc.line_text(line);
568 let start = doc.line_start(line).0;
569 let lead = text.len() - text.trim_start_matches([' ', '\t']).len();
570 let indent = &text[..lead];
571 for blanks in split_points(&text, width.max(1), tab_width) {
572 edits.push(Edit::new(
573 ByteOffset(start + blanks.start),
574 ByteOffset(start + blanks.end),
575 format!("\n{indent}"),
576 ));
577 }
578 }
579 let changes = ChangeSet::new(doc.len(), edits);
580 let after = map_selections_around(sels, &changes);
581 edit_result(changes, after, sels)
582}
583
584fn split_points(text: &str, width: usize, tab_width: usize) -> Vec<Range<usize>> {
587 let cols = |s: &str, from: usize| {
588 s.graphemes(true)
589 .fold(from, |col, g| col + grapheme_width(g, col, tab_width))
590 - from
591 };
592 let blank_end = |at: usize| text.len() - text[at..].trim_start_matches([' ', '\t']).len();
593 let lead = blank_end(0);
594 let indent = cols(&text[..lead], 0);
595 let mut breaks = Vec::new();
596 let (mut col, mut filled) = (indent, false);
599 let mut at = lead;
600 while at < text.len() {
601 let word = blank_end(at);
602 if word == text.len() {
603 break;
604 }
605 let end = text[word..]
606 .find([' ', '\t'])
607 .map_or(text.len(), |i| word + i);
608 let blanks = cols(&text[at..word], col);
609 let word_cols = cols(&text[word..end], col + blanks);
610 if filled && col + blanks + word_cols > width {
611 breaks.push(at..word);
612 col = indent + cols(&text[word..end], indent);
613 } else {
614 col += blanks + word_cols;
615 }
616 filled = true;
617 at = end;
618 }
619 breaks
620}
621
622fn touched_lines(doc: &Document, sels: &SelectionSet) -> BTreeSet<usize> {
623 sels.ranges()
624 .iter()
625 .flat_map(|s| {
626 let (a, b) = lines_of(doc, s);
627 a..=b
628 })
629 .collect()
630}
631
632pub(crate) fn spans_lines(doc: &Document, sels: &SelectionSet) -> bool {
635 sels.ranges().iter().any(|s| {
636 let (a, b) = lines_of(doc, s);
637 !s.is_empty() && a != b
638 })
639}
640
641fn map_selections_around(sels: &SelectionSet, changes: &ChangeSet) -> SelectionSet {
645 sels.map(|s| {
646 let (start_bias, end_bias) = (Bias::Before, Bias::After);
647 let map = |p: ByteOffset, bias| changes.map_position(p, bias);
648 if s.is_empty() {
649 Selection::caret(map(s.head, Bias::After))
650 } else if s.anchor <= s.head {
651 Selection::new(map(s.anchor, start_bias), map(s.head, end_bias))
652 } else {
653 Selection::new(map(s.anchor, end_bias), map(s.head, start_bias))
654 }
655 })
656}
657
658pub(crate) fn indent(doc: &Document, sels: &SelectionSet, unit: &str) -> Resolved {
660 let edits = touched_lines(doc, sels)
661 .into_iter()
662 .filter(|&l| doc.line_start(l) < doc.line_end(l))
663 .map(|l| Edit::new(doc.line_start(l), doc.line_start(l), unit))
664 .collect();
665 let changes = ChangeSet::new(doc.len(), edits);
666 let after = map_selections_around(sels, &changes);
667 edit_result(changes, after, sels)
668}
669
670pub(crate) fn unindent(doc: &Document, sels: &SelectionSet, width: usize) -> Resolved {
673 let edits = touched_lines(doc, sels)
674 .into_iter()
675 .filter_map(|l| {
676 let text = doc.line_text(l);
677 let n = if text.starts_with('\t') {
678 1
679 } else {
680 text.bytes()
681 .take(width.max(1))
682 .take_while(|&b| b == b' ')
683 .count()
684 };
685 let start = doc.line_start(l);
686 (n > 0).then(|| Edit::new(start, ByteOffset(start.0 + n), ""))
687 })
688 .collect();
689 let changes = ChangeSet::new(doc.len(), edits);
690 let after = sels.map(|s| Selection {
691 anchor: changes.map_position(s.anchor, Bias::Before),
692 head: changes.map_position(s.head, Bias::Before),
693 goal: None,
694 });
695 edit_result(changes, after, sels)
696}
697
698fn indent_len(text: &str) -> usize {
700 text.len() - text.trim_start_matches([' ', '\t']).len()
701}
702
703#[derive(Clone, Copy, Debug, PartialEq, Eq)]
705pub(crate) enum CommentOp {
706 Toggle,
708 Add,
710 Remove,
712}
713
714pub(crate) fn line_comment(
718 doc: &Document,
719 sels: &SelectionSet,
720 marker: &str,
721 op: CommentOp,
722) -> Resolved {
723 let lines: Vec<(usize, String)> = touched_lines(doc, sels)
724 .into_iter()
725 .map(|l| (l, doc.line_text(l)))
726 .filter(|(_, t)| !t.trim().is_empty())
727 .collect();
728 if lines.is_empty() {
729 return Resolved::Selections(sels.clone());
730 }
731 let commented = |t: &str| t[indent_len(t)..].starts_with(marker);
732 let remove = match op {
733 CommentOp::Toggle => lines.iter().all(|(_, t)| commented(t)),
734 CommentOp::Add => false,
735 CommentOp::Remove => true,
736 };
737 let edits = if remove {
738 lines
739 .iter()
740 .filter(|(_, t)| commented(t))
741 .map(|(l, t)| {
742 let at = doc.line_start(*l).0 + indent_len(t);
743 let rest = &t[indent_len(t) + marker.len()..];
744 let len = marker.len() + usize::from(rest.starts_with(' '));
745 Edit::new(ByteOffset(at), ByteOffset(at + len), "")
746 })
747 .collect()
748 } else {
749 let col = lines.iter().map(|(_, t)| indent_len(t)).min().unwrap_or(0);
750 lines
751 .iter()
752 .map(|(l, _)| {
753 let at = ByteOffset(doc.line_start(*l).0 + col);
754 Edit::new(at, at, format!("{marker} "))
755 })
756 .collect()
757 };
758 let changes = ChangeSet::new(doc.len(), edits);
759 let after = map_selections_around(sels, &changes);
760 edit_result(changes, after, sels)
761}
762
763pub(crate) fn toggle_block_comment(
766 doc: &Document,
767 sels: &SelectionSet,
768 open: &str,
769 close: &str,
770) -> Resolved {
771 let mut edits = Vec::new();
772 for s in sels.ranges() {
773 let range = if s.is_empty() {
774 let line = doc.line_of(s.head);
775 let text = doc.line_text(line);
776 let start = doc.line_start(line).0;
777 ByteOffset(start + indent_len(&text))..ByteOffset(start + text.trim_end().len())
778 } else {
779 s.range()
780 };
781 if range.start >= range.end {
782 continue;
783 }
784 let text = doc.slice(range.clone());
785 let lead = text.len() - text.trim_start().len();
786 let inner = text.trim();
787 if in_block_comment(&text, open, close) {
788 let start = range.start.0 + lead;
790 let end = start + inner.len();
791 let body = &inner[open.len()..inner.len() - close.len()];
792 let a = open.len() + usize::from(body.starts_with(' '));
793 let b = close.len() + usize::from(body.len() > 1 && body.ends_with(' '));
794 edits.push(Edit::new(ByteOffset(start), ByteOffset(start + a), ""));
795 edits.push(Edit::new(ByteOffset(end - b), ByteOffset(end), ""));
796 } else {
797 edits.push(Edit::new(range.start, range.start, format!("{open} ")));
798 edits.push(Edit::new(range.end, range.end, format!(" {close}")));
799 }
800 }
801 let changes = ChangeSet::new(doc.len(), edits);
802 let after = map_selections_around(sels, &changes);
803 edit_result(changes, after, sels)
804}
805
806fn in_block_comment(text: &str, open: &str, close: &str) -> bool {
808 let inner = text.trim();
809 inner.len() >= open.len() + close.len()
812 && inner.starts_with(open)
813 && inner[open.len()..].find(close) == Some(inner.len() - open.len() - close.len())
814}
815
816pub(crate) fn comment_lines(
819 doc: &Document,
820 sels: &SelectionSet,
821 syntax: &crate::buffer::CommentSyntax,
822 op: CommentOp,
823) -> Option<Resolved> {
824 if let Some(marker) = &syntax.line {
825 return Some(line_comment(doc, sels, marker, op));
826 }
827 let (open, close) = syntax.block.as_ref()?;
828 let text_start = |l: usize| ByteOffset(doc.line_start(l).0 + indent_len(&doc.line_text(l)));
831 let span = |a: usize, b: usize| Selection::new(text_start(a), doc.line_end(b));
832 let is_comment = |s: &Selection| in_block_comment(&doc.slice(s.range()), open, close);
833 let mut runs: Vec<(usize, usize)> = Vec::new();
834 for l in touched_lines(doc, sels) {
835 match runs.last_mut() {
836 Some((_, end)) if *end + 1 == l => *end = l,
837 _ => runs.push((l, l)),
838 }
839 }
840 let mut targets = Vec::new();
842 for (a, b) in runs {
843 let filled: Vec<usize> = (a..=b)
844 .filter(|&l| !doc.line_text(l).trim().is_empty())
845 .collect();
846 let (Some(&first), Some(&last)) = (filled.first(), filled.last()) else {
847 continue;
848 };
849 let whole = span(first, last);
850 let each: Vec<Selection> = filled.iter().map(|&l| span(l, l)).collect();
851 let one = is_comment(&whole);
852 let every = each.iter().all(&is_comment);
853 match op {
854 CommentOp::Add if !one && !every => targets.push(whole),
855 CommentOp::Add => {}
856 _ if one => targets.push(whole),
857 CommentOp::Remove => targets.extend(each.into_iter().filter(&is_comment)),
858 CommentOp::Toggle if every => targets.extend(each),
859 CommentOp::Toggle => targets.push(whole),
860 }
861 }
862 if targets.is_empty() {
863 return Some(Resolved::Selections(sels.clone()));
864 }
865 let targets = SelectionSet::new(targets, 0);
867 match toggle_block_comment(doc, &targets, open, close) {
868 Resolved::Edit { changes, kind, .. } => {
869 let after = map_selections_around(sels, &changes);
870 Some(Resolved::Edit {
871 changes,
872 after,
873 kind,
874 })
875 }
876 other => Some(other),
877 }
878}
879
880fn target_lines(doc: &Document, sels: &SelectionSet) -> Vec<usize> {
884 let mut lines: Vec<usize> = match sels.rect() {
885 Some(r) => (r.top()..=r.bottom()).collect(),
886 None => selected_lines(doc, sels).into_iter().collect(),
887 };
888 let last = doc.line_count() - 1;
889 if last > 0 && lines.last() == Some(&last) && doc.line_text(last).is_empty() {
890 lines.pop();
891 }
892 lines
893}
894
895pub(crate) fn remove_lines(doc: &Document, sels: &SelectionSet, lines: &[usize]) -> Resolved {
901 let last = doc.line_count() - 1;
902 let mut edits = Vec::new();
903 let mut i = 0;
904 while i < lines.len() {
905 let first = lines[i];
906 while i + 1 < lines.len() && lines[i + 1] == lines[i] + 1 {
907 i += 1;
908 }
909 let end = lines[i];
910 i += 1;
911 let range = if end < last {
912 doc.line_start(first)..doc.line_start(end + 1)
913 } else if first > 0 {
914 doc.line_end(first - 1)..doc.len()
915 } else {
916 ByteOffset(0)..doc.len()
917 };
918 if range.start < range.end {
919 edits.push(Edit::new(range.start, range.end, ""));
920 }
921 }
922 let changes = ChangeSet::new(doc.len(), edits);
923 let after = map_before(sels, &changes);
924 edit_result(changes, after, sels)
925}
926
927pub(crate) fn remove_empty(
930 doc: &Document,
931 sels: &SelectionSet,
932 include_whitespace: bool,
933) -> Resolved {
934 let lines: Vec<usize> = target_lines(doc, sels)
935 .into_iter()
936 .filter(|&line| {
937 let text = doc.line_text(line);
938 if include_whitespace {
939 text.trim_matches([' ', '\t']).is_empty()
940 } else {
941 text.is_empty()
942 }
943 })
944 .collect();
945 remove_lines(doc, sels, &lines)
946}
947
948pub(crate) fn remove_duplicates(
952 doc: &Document,
953 sels: &SelectionSet,
954 consecutive_only: bool,
955) -> Resolved {
956 let mut seen = HashSet::new();
957 let mut prev: Option<(usize, String)> = None;
958 let mut lines = Vec::new();
959 for line in target_lines(doc, sels) {
960 let text = doc.line_text(line);
961 let duplicate = if consecutive_only {
962 prev.as_ref()
963 .is_some_and(|(l, t)| l + 1 == line && *t == text)
964 } else {
965 !seen.insert(text.clone())
966 };
967 if duplicate {
968 lines.push(line);
969 }
970 prev = Some((line, text));
971 }
972 remove_lines(doc, sels, &lines)
973}
974
975pub(crate) fn delete_lines(doc: &Document, sels: &SelectionSet) -> Resolved {
978 let lines: Vec<usize> = touched_lines(doc, sels).into_iter().collect();
979 remove_lines(doc, sels, &lines)
980}
981
982pub(crate) fn transpose(doc: &Document, sels: &SelectionSet) -> Resolved {
986 let lines: BTreeSet<usize> = sels.ranges().iter().map(|s| doc.line_of(s.head)).collect();
987 let mut edits = Vec::new();
988 let mut swapped = BTreeSet::new();
989 for line in lines {
990 if line == 0 || swapped.contains(&(line - 1)) {
991 continue;
992 }
993 let above = doc.line_text(line - 1);
994 let this = doc.line_text(line);
995 edits.push(Edit::new(
996 doc.line_start(line - 1),
997 doc.line_end(line),
998 format!("{this}\n{above}"),
999 ));
1000 swapped.insert(line);
1001 }
1002 let changes = ChangeSet::new(doc.len(), edits);
1003 let follow = |pos: ByteOffset| {
1006 for &line in &swapped {
1007 let (above, this) = (doc.line_start(line - 1), doc.line_start(line));
1008 let this_len = doc.line_end(line).0 - this.0;
1009 if above <= pos && pos < this {
1010 return ByteOffset(pos.0 + this_len + 1);
1011 }
1012 if this <= pos && pos <= doc.line_end(line) {
1013 return ByteOffset(above.0 + pos.0 - this.0);
1014 }
1015 }
1016 pos
1017 };
1018 let after = sels.map(|s| {
1019 let line = doc.line_of(s.head);
1020 if swapped.contains(&line) {
1021 let this_len = doc.line_end(line).0 - doc.line_start(line).0;
1022 Selection::caret(ByteOffset(doc.line_start(line - 1).0 + this_len + 1))
1023 } else {
1024 Selection::new(follow(s.anchor), follow(s.head))
1025 }
1026 });
1027 edit_result(changes, after, sels)
1028}
1029
1030pub(crate) fn insert_line(doc: &Document, sels: &SelectionSet, above: bool) -> Resolved {
1034 let line_for = |s: &Selection| {
1035 let (first, last) = lines_of(doc, s);
1036 if above { first } else { last }
1037 };
1038 let lines: BTreeSet<usize> = sels.ranges().iter().map(line_for).collect();
1039 let mut spots = Vec::new();
1041 let mut edits = Vec::new();
1042 for line in lines {
1043 let text = doc.line_text(line);
1044 let indent = &text[..indent_len(&text)];
1045 if above {
1046 let at = doc.line_start(line);
1047 edits.push(Edit::new(at, at, format!("{indent}\n")));
1048 spots.push((line, at, indent.len()));
1049 } else {
1050 let at = doc.line_end(line);
1051 edits.push(Edit::new(at, at, format!("\n{indent}")));
1052 spots.push((line, at, indent.len() + 1));
1053 }
1054 }
1055 let changes = ChangeSet::new(doc.len(), edits);
1056 let primary_line = line_for(sels.primary());
1057 let primary = spots
1058 .iter()
1059 .position(|(line, ..)| *line == primary_line)
1060 .unwrap_or(0);
1061 let carets = spots
1062 .iter()
1063 .map(|&(_, at, offset)| {
1064 Selection::caret(ByteOffset(
1065 changes.map_position(at, Bias::Before).0 + offset,
1066 ))
1067 })
1068 .collect();
1069 edit_result(changes, SelectionSet::new(carets, primary), sels)
1070}
1071
1072pub(crate) fn reverse(doc: &Document, sels: &SelectionSet) -> Resolved {
1075 let rect = sels.rect().cloned();
1076 let any_selected = sels.ranges().iter().any(|s| !s.is_empty());
1077 let (first, mut last) = if let Some(r) = &rect {
1078 (r.top(), r.bottom())
1079 } else if any_selected {
1080 let spans: Vec<_> = sels.ranges().iter().map(|s| lines_of(doc, s)).collect();
1081 let first = spans.iter().map(|s| s.0).min().unwrap_or(0);
1082 let last = spans.iter().map(|s| s.1).max().unwrap_or(0);
1083 (first, last)
1084 } else {
1085 (0, doc.line_count() - 1)
1086 };
1087 if last > first && last + 1 == doc.line_count() && doc.line_text(last).is_empty() {
1089 last -= 1;
1090 }
1091 if last <= first {
1092 return Resolved::Selections(sels.clone());
1093 }
1094 let mut lines: Vec<String> = (first..=last).map(|l| doc.line_text(l)).collect();
1095 lines.reverse();
1096 let new = lines.join("\n");
1097 let (start, end) = (doc.line_start(first), doc.line_end(last));
1098 if doc.slice(start..end) == new {
1099 return Resolved::Selections(sels.clone());
1100 }
1101 let changes = ChangeSet::new(doc.len(), vec![Edit::new(start, end, new.as_str())]);
1102 let after = if any_selected || rect.is_some() {
1103 SelectionSet::single(Selection::new(start, ByteOffset(start.0 + new.len())))
1104 } else {
1105 SelectionSet::caret(start)
1106 };
1107 edit_result(changes, after, sels)
1108}
1109
1110pub(crate) fn trim_lines(doc: &Document, sels: &SelectionSet, trailing: bool) -> Resolved {
1113 let mut edits = Vec::new();
1114 for line in selected_lines(doc, sels) {
1115 let text = doc.line_text(line);
1116 let start = doc.line_start(line).0;
1117 let lead = indent_len(&text);
1118 let keep_end = if trailing {
1120 text.trim_end_matches([' ', '\t']).len().max(lead)
1121 } else {
1122 text.len()
1123 };
1124 if lead > 0 {
1125 edits.push(Edit::new(ByteOffset(start), ByteOffset(start + lead), ""));
1126 }
1127 if keep_end < text.len() {
1128 edits.push(Edit::new(
1129 ByteOffset(start + keep_end),
1130 ByteOffset(start + text.len()),
1131 "",
1132 ));
1133 }
1134 }
1135 let changes = ChangeSet::new(doc.len(), edits);
1136 let after = map_before(sels, &changes);
1137 edit_result(changes, after, sels)
1138}
1139
1140pub(crate) fn eol_to_space(doc: &Document, sels: &SelectionSet) -> Resolved {
1144 let lines = target_lines(doc, sels);
1145 let edits = lines
1146 .windows(2)
1147 .filter(|w| w[1] == w[0] + 1)
1148 .map(|w| Edit::new(doc.line_end(w[0]), doc.line_start(w[1]), " "))
1149 .collect();
1150 let changes = ChangeSet::new(doc.len(), edits);
1151 let after = map_before(sels, &changes);
1152 edit_result(changes, after, sels)
1153}
1154
1155pub(crate) fn tabs_to_spaces(doc: &Document, sels: &SelectionSet, tab_width: usize) -> Resolved {
1158 let width = tab_width.max(1);
1159 let mut edits = Vec::new();
1160 for line in selected_lines(doc, sels) {
1161 let text = doc.line_text(line);
1162 if !text.contains('\t') {
1163 continue;
1164 }
1165 let start = doc.line_start(line).0;
1166 let mut col = 0;
1167 for (i, g) in text.grapheme_indices(true) {
1168 let w = grapheme_width(g, col, width);
1169 if g == "\t" {
1170 edits.push(Edit::new(
1171 ByteOffset(start + i),
1172 ByteOffset(start + i + 1),
1173 " ".repeat(w),
1174 ));
1175 }
1176 col += w;
1177 }
1178 }
1179 let changes = ChangeSet::new(doc.len(), edits);
1180 let after = map_before(sels, &changes);
1181 edit_result(changes, after, sels)
1182}
1183
1184fn spaces_as_tabs(col: usize, n: usize, width: usize) -> Option<String> {
1188 let end = col + n;
1189 let mut out = String::new();
1190 let mut c = col;
1191 let mut changed = false;
1192 while c < end {
1193 let stop = (c / width + 1) * width;
1194 if stop > end {
1195 out.push_str(&" ".repeat(end - c));
1196 break;
1197 }
1198 if stop - c >= 2 {
1199 out.push('\t');
1200 changed = true;
1201 } else {
1202 out.push(' ');
1203 }
1204 c = stop;
1205 }
1206 changed.then_some(out)
1207}
1208
1209pub(crate) fn spaces_to_tabs(doc: &Document, sels: &SelectionSet, tab_width: usize) -> Resolved {
1215 let width = tab_width.max(1);
1216 let mut edits = Vec::new();
1217 for line in selected_lines(doc, sels) {
1218 let text = doc.line_text(line);
1219 if !text.contains(' ') {
1220 continue;
1221 }
1222 let start = doc.line_start(line).0;
1223 let lead = indent_len(&text);
1224 let mut col = 0;
1225 for b in text[..lead].bytes() {
1226 col = if b == b'\t' {
1227 col + width - col % width
1228 } else {
1229 col + 1
1230 };
1231 }
1232 let indent = format!("{}{}", "\t".repeat(col / width), " ".repeat(col % width));
1233 if indent != text[..lead] {
1234 edits.push(Edit::new(
1235 ByteOffset(start),
1236 ByteOffset(start + lead),
1237 indent,
1238 ));
1239 }
1240 let mut run: Option<(usize, usize)> = None;
1242 let flush = |run: &mut Option<(usize, usize)>, col: usize, edits: &mut Vec<Edit>| {
1243 if let Some((byte, from)) = run.take()
1244 && let Some(tabs) = spaces_as_tabs(from, col - from, width)
1245 {
1246 edits.push(Edit::new(
1247 ByteOffset(start + byte),
1248 ByteOffset(start + byte + col - from),
1249 tabs,
1250 ));
1251 }
1252 };
1253 for (i, g) in text[lead..].grapheme_indices(true) {
1254 if g == " " {
1255 run.get_or_insert((lead + i, col));
1256 } else {
1257 flush(&mut run, col, &mut edits);
1258 }
1259 col += grapheme_width(g, col, width);
1260 }
1261 flush(&mut run, col, &mut edits);
1262 }
1263 let changes = ChangeSet::new(doc.len(), edits);
1264 let after = map_before(sels, &changes);
1265 edit_result(changes, after, sels)
1266}
1267
1268#[cfg(test)]
1269mod tests {
1270 use super::*;
1271
1272 #[test]
1273 fn case_modes() {
1274 let s = "hello WORLD, don't straße";
1275 assert_eq!(
1276 convert_case(s, CaseMode::Upper),
1277 "HELLO WORLD, DON'T STRASSE"
1278 );
1279 assert_eq!(
1280 convert_case(s, CaseMode::Lower),
1281 "hello world, don't straße"
1282 );
1283 assert_eq!(
1284 convert_case(s, CaseMode::Title),
1285 "Hello World, Don't Straße"
1286 );
1287 assert_eq!(
1288 convert_case(s, CaseMode::Invert),
1289 "HELLO world, DON'T STRASSE"
1290 );
1291 assert_eq!(
1292 convert_case("hello WORLD, mcDonald", CaseMode::TitleBlend),
1293 "Hello WORLD, McDonald"
1294 );
1295 }
1296
1297 #[test]
1298 fn sentence_case_starts_after_an_end_mark_or_a_blank_line() {
1299 let s = "HELLO there. e.g. THIS one! and? yes\nno\n\nNEW para";
1300 assert_eq!(
1303 convert_case(s, CaseMode::Sentence),
1304 "Hello there. E.g. This one! And? Yes\nno\n\nNew para"
1305 );
1306 assert_eq!(
1307 convert_case(s, CaseMode::SentenceBlend),
1308 "HELLO there. E.g. THIS one! And? Yes\nno\n\nNEW para"
1309 );
1310 assert_eq!(convert_case("", CaseMode::Sentence), "");
1311 assert_eq!(
1312 convert_case("3 apples. ok", CaseMode::Sentence),
1313 "3 Apples. Ok"
1314 );
1315 assert_eq!(
1317 convert_case("it costs 2.50 DOLLARS", CaseMode::Sentence),
1318 "It costs 2.50 dollars"
1319 );
1320 assert_eq!(convert_case("a\n \nb", CaseMode::Sentence), "A\n \nB");
1321 }
1322
1323 #[test]
1324 fn random_case_changes_only_letters_and_follows_the_seed() {
1325 let s = "abcdefghijklmnop 123 straße!";
1326 let a = convert_case(s, CaseMode::Random { seed: 7 });
1327 assert_eq!(a, convert_case(s, CaseMode::Random { seed: 7 }));
1328 assert_ne!(a, convert_case(s, CaseMode::Random { seed: 8 }));
1329 assert_eq!(a.to_lowercase().replace("ss", "ß"), s);
1330 assert!(a.chars().any(char::is_uppercase) && a.chars().any(char::is_lowercase));
1331 }
1332
1333 #[test]
1334 fn spaces_become_tabs_only_where_columns_stay() {
1335 assert_eq!(spaces_as_tabs(1, 3, 4), Some("\t".to_owned()));
1336 assert_eq!(
1337 spaces_as_tabs(3, 1, 4),
1338 None,
1339 "one space before a stop stays"
1340 );
1341 assert_eq!(spaces_as_tabs(2, 8, 4), Some("\t\t ".to_owned()));
1342 assert_eq!(spaces_as_tabs(0, 3, 4), None);
1343 }
1344
1345 #[test]
1346 fn numbers_at_line_starts() {
1347 assert_eq!(leading_number(" 42 apples"), Some(42.0));
1348 assert_eq!(leading_number("-3.5x"), Some(-3.5));
1349 assert_eq!(leading_number("7."), Some(7.0));
1350 assert_eq!(leading_number("x1"), None);
1351 assert_eq!(leading_number("-"), None);
1352 }
1353}