1use std::cmp::Ordering;
6use std::collections::{BTreeSet, HashSet};
7use std::ops::Range;
8
9use serde::{Deserialize, Serialize};
10
11use crate::changeset::{Bias, ChangeSet, Edit};
12use crate::history::EditKind;
13use crate::position::{ByteOffset, visual_col_to_byte, visual_col_to_byte_ceil};
14use crate::resolve::Resolved;
15use crate::selection::{Selection, SelectionSet};
16use crate::text::Document;
17
18#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
19pub enum CaseMode {
20 Upper,
21 Lower,
22 Title,
24 Invert,
26}
27
28#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
29pub struct SortOptions {
30 pub descending: bool,
31 pub case_insensitive: bool,
32 pub numeric: bool,
35 pub remove_duplicates: bool,
37}
38
39pub fn convert_case(text: &str, mode: CaseMode) -> String {
42 match mode {
43 CaseMode::Upper => text.to_uppercase(),
44 CaseMode::Lower => text.to_lowercase(),
45 CaseMode::Invert => text
46 .chars()
47 .flat_map(|c| -> Box<dyn Iterator<Item = char>> {
48 if c.is_uppercase() {
49 Box::new(c.to_lowercase())
50 } else if c.is_lowercase() {
51 Box::new(c.to_uppercase())
52 } else {
53 Box::new(std::iter::once(c))
54 }
55 })
56 .collect(),
57 CaseMode::Title => {
58 let mut out = String::with_capacity(text.len());
59 let mut in_word = false;
60 for c in text.chars() {
61 if c.is_alphanumeric() {
62 if in_word {
63 out.extend(c.to_lowercase());
64 } else {
65 out.extend(c.to_uppercase());
66 }
67 in_word = true;
68 } else {
69 out.push(c);
70 in_word = in_word && (c == '\'' || c == '\u{2019}');
72 }
73 }
74 out
75 }
76 }
77}
78
79fn line_range(doc: &Document, line: usize) -> Range<ByteOffset> {
80 doc.line_start(line)..doc.line_end(line)
81}
82
83fn lines_of(doc: &Document, s: &Selection) -> (usize, usize) {
86 let first = doc.line_of(s.start());
87 let mut last = doc.line_of(s.end());
88 if !s.is_empty() && last > first && s.end() == doc.line_start(last) {
89 last -= 1;
90 }
91 (first, last)
92}
93
94fn merge(mut ranges: Vec<Range<ByteOffset>>) -> Vec<Range<ByteOffset>> {
96 ranges.sort_by_key(|r| (r.start, r.end));
97 let mut out: Vec<Range<ByteOffset>> = Vec::with_capacity(ranges.len());
98 for r in ranges {
99 match out.last_mut() {
100 Some(last) if r.start <= last.end => last.end = last.end.max(r.end),
101 _ => out.push(r),
102 }
103 }
104 out
105}
106
107fn edit_result(changes: ChangeSet, after: SelectionSet, sels: &SelectionSet) -> Resolved {
108 if changes.is_empty() {
109 Resolved::Selections(sels.clone())
110 } else {
111 Resolved::Edit {
112 changes,
113 after,
114 kind: EditKind::Other,
115 }
116 }
117}
118
119fn map_before(sels: &SelectionSet, changes: &ChangeSet) -> SelectionSet {
122 sels.map(|s| Selection {
123 anchor: changes.map_position(s.anchor, Bias::Before),
124 head: changes.map_position(s.head, Bias::Before),
125 goal: None,
126 })
127}
128
129pub(crate) fn case(doc: &Document, sels: &SelectionSet, mode: CaseMode) -> Resolved {
131 let targets = sels
132 .ranges()
133 .iter()
134 .map(|s| {
135 if s.is_empty() {
136 line_range(doc, doc.line_of(s.head))
137 } else {
138 s.range()
139 }
140 })
141 .collect();
142 let pieces: Vec<(Range<ByteOffset>, String, String)> = merge(targets)
143 .into_iter()
144 .map(|r| {
145 let old = doc.slice(r.clone());
146 let new = convert_case(&old, mode);
147 (r, old, new)
148 })
149 .collect();
150 let changes = ChangeSet::new(
151 doc.len(),
152 pieces
153 .iter()
154 .filter(|(_, old, new)| old != new)
155 .map(|(r, _, new)| Edit::new(r.start, r.end, new.as_str()))
156 .collect(),
157 );
158 let remap = |pos: ByteOffset| {
161 let Some((r, old, new)) = pieces.iter().find(|(r, ..)| r.start <= pos && pos <= r.end)
162 else {
163 return changes.map_position(pos, Bias::Before);
164 };
165 let new_start = changes.map_position(r.start, Bias::Before);
166 if pos == r.end {
167 return ByteOffset(new_start.0 + new.len());
168 }
169 let chars = old[..pos.0 - r.start.0].chars().count();
170 let off = new.char_indices().nth(chars).map_or(new.len(), |(i, _)| i);
171 ByteOffset(new_start.0 + off)
172 };
173 let after = sels
174 .map(|s| Selection::new(remap(s.anchor), remap(s.head)))
175 .with_rect(sels.rect().cloned());
176 edit_result(changes, after, sels)
177}
178
179fn leading_number(s: &str) -> Option<f64> {
181 let t = s.trim_start();
182 let b = t.as_bytes();
183 let mut i = 0;
184 if i < b.len() && (b[i] == b'-' || b[i] == b'+') {
185 i += 1;
186 }
187 let digits_start = i;
188 while i < b.len() && b[i].is_ascii_digit() {
189 i += 1;
190 }
191 let mut digits = i - digits_start;
192 if i < b.len() && b[i] == b'.' {
193 let frac_start = i + 1;
194 let mut j = frac_start;
195 while j < b.len() && b[j].is_ascii_digit() {
196 j += 1;
197 }
198 if j > frac_start {
199 digits += j - frac_start;
200 i = j;
201 }
202 }
203 if digits == 0 {
204 return None;
205 }
206 t[..i].parse().ok()
207}
208
209pub(crate) fn sort(
212 doc: &Document,
213 sels: &SelectionSet,
214 o: SortOptions,
215 tab_width: usize,
216) -> Resolved {
217 let rect = sels.rect().cloned();
218 let any_selected = sels.ranges().iter().any(|s| !s.is_empty());
219 let (first, mut last) = if let Some(r) = &rect {
220 (r.top(), r.bottom())
221 } else if any_selected {
222 let spans: Vec<_> = sels.ranges().iter().map(|s| lines_of(doc, s)).collect();
223 let first = spans.iter().map(|s| s.0).min().unwrap_or(0);
224 let last = spans.iter().map(|s| s.1).max().unwrap_or(0);
225 (first, last)
226 } else {
227 (0, doc.line_count() - 1)
228 };
229 if last > first && last + 1 == doc.line_count() && doc.line_text(last).is_empty() {
231 last -= 1;
232 }
233 if last <= first {
234 return Resolved::Selections(sels.clone());
235 }
236 let lines: Vec<String> = (first..=last).map(|l| doc.line_text(l)).collect();
237 let keys: Vec<String> = (first..=last)
238 .zip(&lines)
239 .map(|(line, text)| {
240 let key = match &rect {
241 Some(r) => {
242 let start = doc.line_start(line).0;
243 let right = visual_col_to_byte(doc, line, r.right(), tab_width).0 - start;
244 let left = (visual_col_to_byte_ceil(doc, line, r.left(), tab_width).0 - start)
245 .min(right);
246 text[left..right].to_owned()
247 }
248 None => text.clone(),
249 };
250 if o.case_insensitive {
251 key.to_lowercase()
252 } else {
253 key
254 }
255 })
256 .collect();
257 let compare = |a: usize, b: usize| -> Ordering {
258 let ord = if o.numeric {
259 match (leading_number(&keys[a]), leading_number(&keys[b])) {
260 (Some(x), Some(y)) => x.total_cmp(&y),
261 (None, Some(_)) => Ordering::Less,
262 (Some(_), None) => Ordering::Greater,
263 (None, None) => keys[a].cmp(&keys[b]),
264 }
265 } else {
266 keys[a].cmp(&keys[b])
267 };
268 if o.descending { ord.reverse() } else { ord }
269 };
270 let mut order: Vec<usize> = (0..lines.len()).collect();
271 order.sort_by(|&a, &b| compare(a, b)); if o.remove_duplicates {
273 let same = |a: &str, b: &str| {
274 if o.case_insensitive {
275 a.to_lowercase() == b.to_lowercase()
276 } else {
277 a == b
278 }
279 };
280 order.dedup_by(|b, a| same(&lines[*a], &lines[*b]));
281 }
282 let new: Vec<&str> = order.iter().map(|&i| lines[i].as_str()).collect();
283 let new = new.join("\n");
284 let start = doc.line_start(first);
285 let end = doc.line_end(last);
286 let changes = ChangeSet::new(doc.len(), vec![Edit::new(start, end, new.as_str())]);
287 let after = if any_selected || rect.is_some() {
288 SelectionSet::single(Selection::new(start, ByteOffset(start.0 + new.len())))
289 } else {
290 SelectionSet::caret(start)
291 };
292 edit_result(changes, after, sels)
293}
294
295fn selected_lines(doc: &Document, sels: &SelectionSet) -> BTreeSet<usize> {
297 let selected: BTreeSet<usize> = sels
298 .ranges()
299 .iter()
300 .filter(|s| !s.is_empty())
301 .flat_map(|s| {
302 let (a, b) = lines_of(doc, s);
303 a..=b
304 })
305 .collect();
306 if selected.is_empty() {
307 (0..doc.line_count()).collect()
308 } else {
309 selected
310 }
311}
312
313pub(crate) fn trim_trailing(doc: &Document, sels: &SelectionSet) -> Resolved {
316 let edits = selected_lines(doc, sels)
317 .into_iter()
318 .filter_map(|line| {
319 let text = doc.line_text(line);
320 let keep = text.trim_end_matches([' ', '\t']).len();
321 (keep < text.len()).then(|| {
322 let start = doc.line_start(line).0;
323 Edit::new(ByteOffset(start + keep), doc.line_end(line), "")
324 })
325 })
326 .collect();
327 let changes = ChangeSet::new(doc.len(), edits);
328 let after = map_before(sels, &changes);
329 edit_result(changes, after, sels)
330}
331
332pub(crate) fn duplicate(doc: &Document, sels: &SelectionSet) -> Resolved {
335 let mut lines = BTreeSet::new();
336 let mut edits = Vec::new();
337 for s in sels.ranges() {
338 if s.is_empty() {
339 lines.insert(doc.line_of(s.head));
340 } else {
341 edits.push(Edit::new(s.end(), s.end(), doc.slice(s.range())));
342 }
343 }
344 for line in lines {
345 let end = doc.line_end(line);
346 edits.push(Edit::new(end, end, format!("\n{}", doc.line_text(line))));
347 }
348 let changes = ChangeSet::new(doc.len(), edits);
349 let after = map_before(sels, &changes);
350 edit_result(changes, after, sels)
351}
352
353pub(crate) fn move_lines(doc: &Document, sels: &SelectionSet, up: bool) -> Resolved {
356 let spans: Vec<_> = sels.ranges().iter().map(|s| lines_of(doc, s)).collect();
357 let first = spans.iter().map(|s| s.0).min().unwrap_or(0);
358 let last = spans.iter().map(|s| s.1).max().unwrap_or(0);
359 let block = doc.slice(doc.line_start(first)..doc.line_end(last));
360 let (edit, shift): (Edit, isize) = if up {
361 if first == 0 {
362 return Resolved::Selections(sels.clone());
363 }
364 let prev = doc.line_text(first - 1);
365 (
366 Edit::new(
367 doc.line_start(first - 1),
368 doc.line_end(last),
369 format!("{block}\n{prev}"),
370 ),
371 -(prev.len() as isize + 1),
372 )
373 } else {
374 if last + 1 >= doc.line_count() {
375 return Resolved::Selections(sels.clone());
376 }
377 let next = doc.line_text(last + 1);
378 (
379 Edit::new(
380 doc.line_start(first),
381 doc.line_end(last + 1),
382 format!("{next}\n{block}"),
383 ),
384 next.len() as isize + 1,
385 )
386 };
387 let changes = ChangeSet::new(doc.len(), vec![edit]);
388 let new_len = changes.new_len().0 as isize;
393 let shift_pos = |p: ByteOffset| ByteOffset((p.0 as isize + shift).clamp(0, new_len) as usize);
394 let after = sels.map(|s| Selection::new(shift_pos(s.anchor), shift_pos(s.head)));
395 edit_result(changes, after, sels)
396}
397
398pub(crate) fn join(doc: &Document, sels: &SelectionSet) -> Resolved {
401 let last_line = doc.line_count() - 1;
402 let mut breaks = BTreeSet::new();
403 for s in sels.ranges() {
404 let (a, b) = lines_of(doc, s);
405 let b = if a == b { (a + 1).min(last_line) } else { b };
406 breaks.extend(a..b);
407 }
408 let edits = breaks
409 .into_iter()
410 .map(|line| {
411 let text = doc.line_text(line);
412 let next = doc.line_text(line + 1);
413 let kept = text.trim_end_matches([' ', '\t']);
414 let rest = next.trim_start_matches([' ', '\t']);
415 let start = ByteOffset(doc.line_start(line).0 + kept.len());
416 let end = ByteOffset(doc.line_start(line + 1).0 + (next.len() - rest.len()));
417 let sep = if kept.is_empty() || rest.is_empty() {
418 ""
419 } else {
420 " "
421 };
422 Edit::new(start, end, sep)
423 })
424 .collect();
425 let changes = ChangeSet::new(doc.len(), edits);
426 let after = map_before(sels, &changes);
427 edit_result(changes, after, sels)
428}
429
430fn target_lines(doc: &Document, sels: &SelectionSet) -> Vec<usize> {
434 let mut lines: Vec<usize> = match sels.rect() {
435 Some(r) => (r.top()..=r.bottom()).collect(),
436 None => selected_lines(doc, sels).into_iter().collect(),
437 };
438 let last = doc.line_count() - 1;
439 if last > 0 && lines.last() == Some(&last) && doc.line_text(last).is_empty() {
440 lines.pop();
441 }
442 lines
443}
444
445fn remove_lines(doc: &Document, sels: &SelectionSet, lines: &[usize]) -> Resolved {
449 let last = doc.line_count() - 1;
450 let mut edits = Vec::new();
451 let mut i = 0;
452 while i < lines.len() {
453 let first = lines[i];
454 while i + 1 < lines.len() && lines[i + 1] == lines[i] + 1 {
455 i += 1;
456 }
457 let end = lines[i];
458 i += 1;
459 let range = if end < last {
460 doc.line_start(first)..doc.line_start(end + 1)
461 } else if first > 0 {
462 doc.line_end(first - 1)..doc.len()
463 } else {
464 ByteOffset(0)..doc.len()
465 };
466 if range.start < range.end {
467 edits.push(Edit::new(range.start, range.end, ""));
468 }
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 remove_empty(
478 doc: &Document,
479 sels: &SelectionSet,
480 include_whitespace: bool,
481) -> Resolved {
482 let lines: Vec<usize> = target_lines(doc, sels)
483 .into_iter()
484 .filter(|&line| {
485 let text = doc.line_text(line);
486 if include_whitespace {
487 text.trim_matches([' ', '\t']).is_empty()
488 } else {
489 text.is_empty()
490 }
491 })
492 .collect();
493 remove_lines(doc, sels, &lines)
494}
495
496pub(crate) fn remove_duplicates(
500 doc: &Document,
501 sels: &SelectionSet,
502 consecutive_only: bool,
503) -> Resolved {
504 let mut seen = HashSet::new();
505 let mut prev: Option<(usize, String)> = None;
506 let mut lines = Vec::new();
507 for line in target_lines(doc, sels) {
508 let text = doc.line_text(line);
509 let duplicate = if consecutive_only {
510 prev.as_ref()
511 .is_some_and(|(l, t)| l + 1 == line && *t == text)
512 } else {
513 !seen.insert(text.clone())
514 };
515 if duplicate {
516 lines.push(line);
517 }
518 prev = Some((line, text));
519 }
520 remove_lines(doc, sels, &lines)
521}
522
523#[cfg(test)]
524mod tests {
525 use super::*;
526
527 #[test]
528 fn case_modes() {
529 let s = "hello WORLD, don't straße";
530 assert_eq!(
531 convert_case(s, CaseMode::Upper),
532 "HELLO WORLD, DON'T STRASSE"
533 );
534 assert_eq!(
535 convert_case(s, CaseMode::Lower),
536 "hello world, don't straße"
537 );
538 assert_eq!(
539 convert_case(s, CaseMode::Title),
540 "Hello World, Don't Straße"
541 );
542 assert_eq!(
543 convert_case(s, CaseMode::Invert),
544 "HELLO world, DON'T STRASSE"
545 );
546 }
547
548 #[test]
549 fn numbers_at_line_starts() {
550 assert_eq!(leading_number(" 42 apples"), Some(42.0));
551 assert_eq!(leading_number("-3.5x"), Some(-3.5));
552 assert_eq!(leading_number("7."), Some(7.0));
553 assert_eq!(leading_number("x1"), None);
554 assert_eq!(leading_number("-"), None);
555 }
556}