diff --git a/src/display_list/from_snippet.rs b/src/display_list/from_snippet.rs index 272c063..f81d38a 100644 --- a/src/display_list/from_snippet.rs +++ b/src/display_list/from_snippet.rs @@ -293,11 +293,23 @@ fn format_body( let mut body = vec![]; let mut current_line = slice.line_start; let mut current_index = 0; - let mut line_index_ranges = vec![]; + let mut line_info = vec![]; + + struct LineInfo { + line_start_index: usize, + line_end_index: usize, + // How many spaces each character in the line take up when displayed + char_widths: Vec, + } for (line, end_line) in CursorLines::new(slice.source) { let line_length = line.chars().count(); let line_range = (current_index, current_index + line_length); + let char_widths = line + .chars() + .map(|c| unicode_width::UnicodeWidthChar::width(c).unwrap_or(0)) + .chain(std::iter::once(1)) // treat the end of line as signle-width + .collect::>(); body.push(DisplayLine::Source { lineno: Some(current_line), inline_marks: vec![], @@ -306,16 +318,28 @@ fn format_body( range: line_range, }, }); - line_index_ranges.push(line_range); + line_info.push(LineInfo { + line_start_index: line_range.0, + line_end_index: line_range.1, + char_widths, + }); current_line += 1; current_index += line_length + end_line as usize; } let mut annotation_line_count = 0; let mut annotations = slice.annotations; - for (idx, (line_start, line_end)) in line_index_ranges.into_iter().enumerate() { + for ( + idx, + LineInfo { + line_start_index, + line_end_index, + char_widths, + }, + ) in line_info.into_iter().enumerate() + { let margin_left = margin - .map(|m| m.left(line_end - line_start)) + .map(|m| m.left(line_end_index - line_start_index)) .unwrap_or_default(); // It would be nice to use filter_drain here once it's stable. annotations = annotations @@ -328,15 +352,22 @@ fn format_body( _ => DisplayAnnotationType::from(annotation.annotation_type), }; match annotation.range { - (start, _) if start > line_end => true, + (start, _) if start > line_end_index => true, (start, end) - if start >= line_start && end <= line_end - || start == line_end && end - start <= 1 => + if start >= line_start_index && end <= line_end_index + || start == line_end_index && end - start <= 1 => { - let range = ( - (start - line_start) - margin_left, - (end - line_start) - margin_left, - ); + let annotation_start_col = char_widths + .iter() + .take(start - line_start_index) + .sum::() + - margin_left; + let annotation_end_col = char_widths + .iter() + .take(end - line_start_index) + .sum::() + - margin_left; + let range = (annotation_start_col, annotation_end_col); body.insert( body_idx + 1, DisplayLine::Source { @@ -359,8 +390,12 @@ fn format_body( annotation_line_count += 1; false } - (start, end) if start >= line_start && start <= line_end && end > line_end => { - if start - line_start == 0 { + (start, end) + if start >= line_start_index + && start <= line_end_index + && end > line_end_index => + { + if start - line_start_index == 0 { if let DisplayLine::Source { ref mut inline_marks, .. @@ -374,7 +409,11 @@ fn format_body( }); } } else { - let range = (start - line_start, start - line_start + 1); + let annotation_start_col = char_widths + .iter() + .take(start - line_start_index) + .sum::(); + let range = (annotation_start_col, annotation_start_col + 1); body.insert( body_idx + 1, DisplayLine::Source { @@ -398,7 +437,7 @@ fn format_body( } true } - (start, end) if start < line_start && end > line_end => { + (start, end) if start < line_start_index && end > line_end_index => { if let DisplayLine::Source { ref mut inline_marks, .. @@ -413,7 +452,11 @@ fn format_body( } true } - (start, end) if start < line_start && end >= line_start && end <= line_end => { + (start, end) + if start < line_start_index + && end >= line_start_index + && end <= line_end_index => + { if let DisplayLine::Source { ref mut inline_marks, .. @@ -427,11 +470,12 @@ fn format_body( }); } - let end_mark = (end - line_start).saturating_sub(1); - let range = ( - end_mark - margin_left, - (end_mark + 1) - margin_left, - ); + let end_mark = char_widths + .iter() + .take(end - line_start_index) + .sum::() + .saturating_sub(1); + let range = (end_mark - margin_left, (end_mark + 1) - margin_left); body.insert( body_idx + 1, DisplayLine::Source { diff --git a/tests/formatter.rs b/tests/formatter.rs index 5c7211d..b1392a1 100644 --- a/tests/formatter.rs +++ b/tests/formatter.rs @@ -550,3 +550,126 @@ fn test_i_29() { assert_eq!(DisplayList::from(snippets).to_string(), expected); } + +#[test] +fn test_point_to_double_width_characters() { + let snippets = Snippet { + slices: vec![snippet::Slice { + source: "こんにちは、世界", + line_start: 1, + origin: Some(""), + annotations: vec![snippet::SourceAnnotation { + range: (6, 8), + label: "world", + annotation_type: snippet::AnnotationType::Error, + }], + fold: false, + }], + title: None, + footer: vec![], + opt: Default::default(), + }; + + let expected = r#" --> :1:7 + | +1 | こんにちは、世界 + | ^^^^ world + |"#; + + assert_eq!(DisplayList::from(snippets).to_string(), expected); +} + +#[test] +fn test_point_to_double_width_characters_across_lines() { + let snippets = Snippet { + slices: vec![snippet::Slice { + source: "おはよう\nございます", + line_start: 1, + origin: Some(""), + annotations: vec![snippet::SourceAnnotation { + range: (2, 8), + label: "Good morning", + annotation_type: snippet::AnnotationType::Error, + }], + fold: false, + }], + title: None, + footer: vec![], + opt: Default::default(), + }; + + let expected = r#" --> :1:3 + | +1 | おはよう + | _____^ +2 | | ございます + | |______^ Good morning + |"#; + + assert_eq!(DisplayList::from(snippets).to_string(), expected); +} + +#[test] +fn test_point_to_double_width_characters_multiple() { + let snippets = Snippet { + slices: vec![snippet::Slice { + source: "お寿司\n食べたい🍣", + line_start: 1, + origin: Some(""), + annotations: vec![ + snippet::SourceAnnotation { + range: (0, 3), + label: "Sushi1", + annotation_type: snippet::AnnotationType::Error, + }, + snippet::SourceAnnotation { + range: (6, 8), + label: "Sushi2", + annotation_type: snippet::AnnotationType::Note, + }, + ], + fold: false, + }], + title: None, + footer: vec![], + opt: Default::default(), + }; + + let expected = r#" --> :1:1 + | +1 | お寿司 + | ^^^^^^ Sushi1 +2 | 食べたい🍣 + | ---- note: Sushi2 + |"#; + + assert_eq!(DisplayList::from(snippets).to_string(), expected); +} + +#[test] +fn test_point_to_double_width_characters_mixed() { + let snippets = Snippet { + slices: vec![snippet::Slice { + source: "こんにちは、新しいWorld!", + line_start: 1, + origin: Some(""), + annotations: vec![snippet::SourceAnnotation { + range: (6, 14), + label: "New world", + annotation_type: snippet::AnnotationType::Error, + }], + fold: false, + }], + title: None, + footer: vec![], + opt: Default::default(), + }; + + let expected = r#" --> :1:7 + | +1 | こんにちは、新しいWorld! + | ^^^^^^^^^^^ New world + |"#; + + assert_eq!(DisplayList::from(snippets).to_string(), expected); +}