|
1 |
| -use crate::renderer::{char_width, num_overlap, LineAnnotation, LineAnnotationType}; |
2 |
| -use crate::{Annotation, AnnotationKind}; |
| 1 | +use crate::renderer::{char_width, is_different, num_overlap, LineAnnotation, LineAnnotationType}; |
| 2 | +use crate::{Annotation, AnnotationKind, Patch}; |
3 | 3 | use std::cmp::{max, min};
|
4 | 4 | use std::ops::Range;
|
5 | 5 |
|
6 | 6 | #[derive(Debug)]
|
7 | 7 | pub(crate) struct SourceMap<'a> {
|
8 | 8 | lines: Vec<LineInfo<'a>>,
|
9 |
| - source: &'a str, |
| 9 | + pub(crate) source: &'a str, |
10 | 10 | }
|
11 | 11 |
|
12 | 12 | impl<'a> SourceMap<'a> {
|
@@ -101,6 +101,26 @@ impl<'a> SourceMap<'a> {
|
101 | 101 | (start, end)
|
102 | 102 | }
|
103 | 103 |
|
| 104 | + pub(crate) fn span_to_snippet(&self, span: Range<usize>) -> Option<&str> { |
| 105 | + self.source.get(span) |
| 106 | + } |
| 107 | + |
| 108 | + pub(crate) fn span_to_lines(&self, span: Range<usize>) -> Vec<&LineInfo<'a>> { |
| 109 | + let mut lines = vec![]; |
| 110 | + let start = span.start; |
| 111 | + let end = span.end; |
| 112 | + for line_info in &self.lines { |
| 113 | + if start >= line_info.end_byte { |
| 114 | + continue; |
| 115 | + } |
| 116 | + if end <= line_info.start_byte { |
| 117 | + break; |
| 118 | + } |
| 119 | + lines.push(line_info); |
| 120 | + } |
| 121 | + lines |
| 122 | + } |
| 123 | + |
104 | 124 | pub(crate) fn annotated_lines(
|
105 | 125 | &self,
|
106 | 126 | annotations: Vec<Annotation<'a>>,
|
@@ -130,7 +150,7 @@ impl<'a> SourceMap<'a> {
|
130 | 150 | let mut multiline_annotations = vec![];
|
131 | 151 |
|
132 | 152 | for Annotation { range, label, kind } in annotations {
|
133 |
| - let (lo, mut hi) = self.span_to_locations(range); |
| 153 | + let (lo, mut hi) = self.span_to_locations(range.clone()); |
134 | 154 |
|
135 | 155 | // Watch out for "empty spans". If we get a span like 6..6, we
|
136 | 156 | // want to just display a `^` at 6, so convert that to
|
@@ -303,6 +323,176 @@ impl<'a> SourceMap<'a> {
|
303 | 323 | annotated_line_infos.sort_by_key(|l| l.line_index);
|
304 | 324 | }
|
305 | 325 | }
|
| 326 | + |
| 327 | + pub(crate) fn splice_lines<'b>( |
| 328 | + &'b self, |
| 329 | + mut patches: Vec<Patch<'b>>, |
| 330 | + ) -> Vec<(String, Vec<Patch<'b>>, Vec<Vec<SubstitutionHighlight>>)> { |
| 331 | + fn push_trailing( |
| 332 | + buf: &mut String, |
| 333 | + line_opt: Option<&str>, |
| 334 | + lo: &Loc, |
| 335 | + hi_opt: Option<&Loc>, |
| 336 | + ) -> usize { |
| 337 | + let mut line_count = 0; |
| 338 | + // Convert CharPos to Usize, as CharPose is character offset |
| 339 | + // Extract low index and high index |
| 340 | + let (lo, hi_opt) = (lo.char, hi_opt.map(|hi| hi.char)); |
| 341 | + if let Some(line) = line_opt { |
| 342 | + if let Some(lo) = line.char_indices().map(|(i, _)| i).nth(lo) { |
| 343 | + // Get high index while account for rare unicode and emoji with char_indices |
| 344 | + let hi_opt = hi_opt.and_then(|hi| line.char_indices().map(|(i, _)| i).nth(hi)); |
| 345 | + match hi_opt { |
| 346 | + // If high index exist, take string from low to high index |
| 347 | + Some(hi) if hi > lo => { |
| 348 | + // count how many '\n' exist |
| 349 | + line_count = line[lo..hi].matches('\n').count(); |
| 350 | + buf.push_str(&line[lo..hi]); |
| 351 | + } |
| 352 | + Some(_) => (), |
| 353 | + // If high index absence, take string from low index till end string.len |
| 354 | + None => { |
| 355 | + // count how many '\n' exist |
| 356 | + line_count = line[lo..].matches('\n').count(); |
| 357 | + buf.push_str(&line[lo..]); |
| 358 | + } |
| 359 | + } |
| 360 | + } |
| 361 | + // If high index is None |
| 362 | + if hi_opt.is_none() { |
| 363 | + buf.push('\n'); |
| 364 | + } |
| 365 | + } |
| 366 | + line_count |
| 367 | + } |
| 368 | + // Assumption: all spans are in the same file, and all spans |
| 369 | + // are disjoint. Sort in ascending order. |
| 370 | + patches.sort_by_key(|p| p.range.start); |
| 371 | + |
| 372 | + // Find the bounding span. |
| 373 | + let Some(lo) = patches.iter().map(|p| p.range.start).min() else { |
| 374 | + return Vec::new(); |
| 375 | + }; |
| 376 | + let Some(hi) = patches.iter().map(|p| p.range.end).max() else { |
| 377 | + return Vec::new(); |
| 378 | + }; |
| 379 | + |
| 380 | + let lines = self.span_to_lines(lo..hi); |
| 381 | + |
| 382 | + let mut highlights = vec![]; |
| 383 | + // To build up the result, we do this for each span: |
| 384 | + // - push the line segment trailing the previous span |
| 385 | + // (at the beginning a "phantom" span pointing at the start of the line) |
| 386 | + // - push lines between the previous and current span (if any) |
| 387 | + // - if the previous and current span are not on the same line |
| 388 | + // push the line segment leading up to the current span |
| 389 | + // - splice in the span substitution |
| 390 | + // |
| 391 | + // Finally push the trailing line segment of the last span |
| 392 | + let (mut prev_hi, _) = self.span_to_locations(lo..hi); |
| 393 | + prev_hi.char = 0; |
| 394 | + let mut prev_line = lines.first().map(|line| line.line); |
| 395 | + let mut buf = String::new(); |
| 396 | + |
| 397 | + let mut line_highlight = vec![]; |
| 398 | + // We need to keep track of the difference between the existing code and the added |
| 399 | + // or deleted code in order to point at the correct column *after* substitution. |
| 400 | + let mut acc = 0; |
| 401 | + for part in &mut patches { |
| 402 | + // If this is a replacement of, e.g. `"a"` into `"ab"`, adjust the |
| 403 | + // suggestion and snippet to look as if we just suggested to add |
| 404 | + // `"b"`, which is typically much easier for the user to understand. |
| 405 | + part.trim_trivial_replacements(self); |
| 406 | + let (cur_lo, cur_hi) = self.span_to_locations(part.range.clone()); |
| 407 | + if prev_hi.line == cur_lo.line { |
| 408 | + let mut count = push_trailing(&mut buf, prev_line, &prev_hi, Some(&cur_lo)); |
| 409 | + while count > 0 { |
| 410 | + highlights.push(std::mem::take(&mut line_highlight)); |
| 411 | + acc = 0; |
| 412 | + count -= 1; |
| 413 | + } |
| 414 | + } else { |
| 415 | + acc = 0; |
| 416 | + highlights.push(std::mem::take(&mut line_highlight)); |
| 417 | + let mut count = push_trailing(&mut buf, prev_line, &prev_hi, None); |
| 418 | + while count > 0 { |
| 419 | + highlights.push(std::mem::take(&mut line_highlight)); |
| 420 | + count -= 1; |
| 421 | + } |
| 422 | + // push lines between the previous and current span (if any) |
| 423 | + for idx in prev_hi.line + 1..(cur_lo.line) { |
| 424 | + if let Some(line) = self.get_line(idx) { |
| 425 | + buf.push_str(line.as_ref()); |
| 426 | + buf.push('\n'); |
| 427 | + highlights.push(std::mem::take(&mut line_highlight)); |
| 428 | + } |
| 429 | + } |
| 430 | + if let Some(cur_line) = self.get_line(cur_lo.line) { |
| 431 | + let end = match cur_line.char_indices().nth(cur_lo.char) { |
| 432 | + Some((i, _)) => i, |
| 433 | + None => cur_line.len(), |
| 434 | + }; |
| 435 | + buf.push_str(&cur_line[..end]); |
| 436 | + } |
| 437 | + } |
| 438 | + // Add a whole line highlight per line in the snippet. |
| 439 | + let len: isize = part |
| 440 | + .replacement |
| 441 | + .split('\n') |
| 442 | + .next() |
| 443 | + .unwrap_or(part.replacement) |
| 444 | + .chars() |
| 445 | + .map(|c| match c { |
| 446 | + '\t' => 4, |
| 447 | + _ => 1, |
| 448 | + }) |
| 449 | + .sum(); |
| 450 | + if !is_different(self, part.replacement, part.range.clone()) { |
| 451 | + // Account for cases where we are suggesting the same code that's already |
| 452 | + // there. This shouldn't happen often, but in some cases for multipart |
| 453 | + // suggestions it's much easier to handle it here than in the origin. |
| 454 | + } else { |
| 455 | + line_highlight.push(SubstitutionHighlight { |
| 456 | + start: (cur_lo.char as isize + acc) as usize, |
| 457 | + end: (cur_lo.char as isize + acc + len) as usize, |
| 458 | + }); |
| 459 | + } |
| 460 | + buf.push_str(part.replacement); |
| 461 | + // Account for the difference between the width of the current code and the |
| 462 | + // snippet being suggested, so that the *later* suggestions are correctly |
| 463 | + // aligned on the screen. Note that cur_hi and cur_lo can be on different |
| 464 | + // lines, so cur_hi.col can be smaller than cur_lo.col |
| 465 | + acc += len - (cur_hi.char as isize - cur_lo.char as isize); |
| 466 | + prev_hi = cur_hi; |
| 467 | + prev_line = self.get_line(prev_hi.line); |
| 468 | + for line in part.replacement.split('\n').skip(1) { |
| 469 | + acc = 0; |
| 470 | + highlights.push(std::mem::take(&mut line_highlight)); |
| 471 | + let end: usize = line |
| 472 | + .chars() |
| 473 | + .map(|c| match c { |
| 474 | + '\t' => 4, |
| 475 | + _ => 1, |
| 476 | + }) |
| 477 | + .sum(); |
| 478 | + line_highlight.push(SubstitutionHighlight { start: 0, end }); |
| 479 | + } |
| 480 | + } |
| 481 | + highlights.push(std::mem::take(&mut line_highlight)); |
| 482 | + // if the replacement already ends with a newline, don't print the next line |
| 483 | + if !buf.ends_with('\n') { |
| 484 | + push_trailing(&mut buf, prev_line, &prev_hi, None); |
| 485 | + } |
| 486 | + // remove trailing newlines |
| 487 | + while buf.ends_with('\n') { |
| 488 | + buf.pop(); |
| 489 | + } |
| 490 | + if highlights.iter().all(|parts| parts.is_empty()) { |
| 491 | + Vec::new() |
| 492 | + } else { |
| 493 | + vec![(buf, patches, highlights)] |
| 494 | + } |
| 495 | + } |
306 | 496 | }
|
307 | 497 |
|
308 | 498 | #[derive(Clone, Debug, PartialOrd, Ord, PartialEq, Eq)]
|
@@ -451,3 +641,11 @@ impl<'a> Iterator for CursorLines<'a> {
|
451 | 641 | }
|
452 | 642 | }
|
453 | 643 | }
|
| 644 | + |
| 645 | +/// Used to translate between `Span`s and byte positions within a single output line in highlighted |
| 646 | +/// code of structured suggestions. |
| 647 | +#[derive(Debug, Clone, Copy)] |
| 648 | +pub(crate) struct SubstitutionHighlight { |
| 649 | + pub(crate) start: usize, |
| 650 | + pub(crate) end: usize, |
| 651 | +} |
0 commit comments