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