Skip to content

Commit 13c0ac7

Browse files
committed
feat: Add Suggestions
1 parent 88f515c commit 13c0ac7

File tree

7 files changed

+1684
-9
lines changed

7 files changed

+1684
-9
lines changed

src/renderer/mod.rs

+615-3
Large diffs are not rendered by default.

src/renderer/source_map.rs

+202-4
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
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};
33
use std::cmp::{max, min};
44
use std::ops::Range;
55

66
#[derive(Debug)]
77
pub(crate) struct SourceMap<'a> {
88
lines: Vec<LineInfo<'a>>,
9-
source: &'a str,
9+
pub(crate) source: &'a str,
1010
}
1111

1212
impl<'a> SourceMap<'a> {
@@ -101,6 +101,26 @@ impl<'a> SourceMap<'a> {
101101
(start, end)
102102
}
103103

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+
104124
pub(crate) fn annotated_lines(
105125
&self,
106126
annotations: Vec<Annotation<'a>>,
@@ -130,7 +150,7 @@ impl<'a> SourceMap<'a> {
130150
let mut multiline_annotations = vec![];
131151

132152
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());
134154

135155
// Watch out for "empty spans". If we get a span like 6..6, we
136156
// want to just display a `^` at 6, so convert that to
@@ -303,6 +323,176 @@ impl<'a> SourceMap<'a> {
303323
annotated_line_infos.sort_by_key(|l| l.line_index);
304324
}
305325
}
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+
}
306496
}
307497

308498
#[derive(Clone, Debug, PartialOrd, Ord, PartialEq, Eq)]
@@ -451,3 +641,11 @@ impl<'a> Iterator for CursorLines<'a> {
451641
}
452642
}
453643
}
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+
}

src/renderer/styled_buffer.rs

+29
Original file line numberDiff line numberDiff line change
@@ -118,4 +118,33 @@ impl StyledBuffer {
118118
pub(crate) fn num_lines(&self) -> usize {
119119
self.lines.len()
120120
}
121+
122+
/// Set `style` for `line`, `col_start..col_end` range if:
123+
/// 1. That line and column range exist in `StyledBuffer`
124+
/// 2. `overwrite` is `true` or existing style is `Style::NoStyle` or `Style::Quotation`
125+
pub(crate) fn set_style_range(
126+
&mut self,
127+
line: usize,
128+
col_start: usize,
129+
col_end: usize,
130+
style: ElementStyle,
131+
overwrite: bool,
132+
) {
133+
for col in col_start..col_end {
134+
self.set_style(line, col, style, overwrite);
135+
}
136+
}
137+
138+
/// Set `style` for `line`, `col` if:
139+
/// 1. That line and column exist in `StyledBuffer`
140+
/// 2. `overwrite` is `true` or existing style is `Style::NoStyle` or `Style::Quotation`
141+
pub(crate) fn set_style(&mut self, line: usize, col: usize, style: ElementStyle, overwrite: bool) {
142+
if let Some(ref mut line) = self.lines.get_mut(line) {
143+
if let Some(StyledChar { style: s, .. }) = line.get_mut(col) {
144+
if overwrite || matches!(s, ElementStyle::NoStyle | ElementStyle::Quotation) {
145+
*s = style;
146+
}
147+
}
148+
}
149+
}
121150
}

src/renderer/stylesheet.rs

+4
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ pub(crate) struct Stylesheet {
1111
pub(crate) emphasis: Style,
1212
pub(crate) none: Style,
1313
pub(crate) context: Style,
14+
pub(crate) addition: Style,
15+
pub(crate) removal: Style,
1416
}
1517

1618
impl Default for Stylesheet {
@@ -31,6 +33,8 @@ impl Stylesheet {
3133
emphasis: Style::new(),
3234
none: Style::new(),
3335
context: Style::new(),
36+
addition: Style::new(),
37+
removal: Style::new(),
3438
}
3539
}
3640
}

0 commit comments

Comments
 (0)