mirror of
https://github.com/openharmony/third_party_rust_codespan.git
synced 2026-07-19 16:43:33 -04:00
Merge pull request #252 from brendanzab/fix-tab-rendering
Fix rendering of tabulation columns
This commit is contained in:
@@ -1,4 +1,3 @@
|
||||
use std::io;
|
||||
use termcolor::{Color, ColorSpec};
|
||||
|
||||
use crate::diagnostic::{LabelStyle, Severity};
|
||||
@@ -31,56 +30,6 @@ impl Default for Config {
|
||||
}
|
||||
}
|
||||
|
||||
impl Config {
|
||||
/// Measure the unicode width of a character, taking into account the tab width.
|
||||
pub fn width(&self, ch: char) -> usize {
|
||||
use unicode_width::UnicodeWidthChar;
|
||||
|
||||
match ch {
|
||||
'\t' => self.tab_width,
|
||||
_ => ch.width().unwrap_or(0),
|
||||
}
|
||||
}
|
||||
|
||||
/// Construct a source writer using the current config.
|
||||
pub fn source<'a, W: ?Sized>(&self, writer: &'a mut W) -> SourceWriter<&'a mut W> {
|
||||
SourceWriter {
|
||||
upstream: writer,
|
||||
tab_width: self.tab_width,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Writer that replaces tab characters with the configured number of spaces.
|
||||
pub struct SourceWriter<W> {
|
||||
upstream: W,
|
||||
tab_width: usize,
|
||||
}
|
||||
|
||||
impl<W: io::Write> io::Write for SourceWriter<W> {
|
||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||
let mut last_term = 0usize;
|
||||
for (i, ch) in buf.iter().enumerate() {
|
||||
if *ch == b'\t' {
|
||||
self.upstream.write_all(&buf[last_term..i])?;
|
||||
last_term = i + 1;
|
||||
write!(
|
||||
self.upstream,
|
||||
"{space: >width$}",
|
||||
space = "",
|
||||
width = self.tab_width,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
self.upstream.write_all(&buf[last_term..])?;
|
||||
Ok(buf.len())
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
self.upstream.flush()
|
||||
}
|
||||
}
|
||||
|
||||
/// The display style to use when rendering diagnostics.
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum DisplayStyle {
|
||||
|
||||
@@ -71,7 +71,6 @@ type Underline = (LabelStyle, VerticalBound);
|
||||
/// │ │ │ │ │
|
||||
/// ┌────────────────────────────────────────────────────────────────────────────
|
||||
/// header ── │ error[0001]: oh noes, a cupcake has occurred!
|
||||
/// empty ── │
|
||||
/// snippet start ── │ ┌─ test:9:0
|
||||
/// snippet empty ── │ │
|
||||
/// snippet line ── │ 9 │ ╭ Cupcake ipsum dolor. Sit amet marshmallow topping cheesecake
|
||||
@@ -265,8 +264,14 @@ impl<'writer, 'config> Renderer<'writer, 'config> {
|
||||
}
|
||||
}
|
||||
|
||||
// Write source
|
||||
write!(self.config.source(self.writer), " {}", source)?;
|
||||
// Write source text
|
||||
write!(self, " ")?;
|
||||
for (metrics, ch) in self.char_metrics(source.char_indices()) {
|
||||
match ch {
|
||||
'\t' => (0..metrics.unicode_width).try_for_each(|_| write!(self, " "))?,
|
||||
_ => write!(self, "{}", ch)?,
|
||||
}
|
||||
}
|
||||
write!(self, "\n")?;
|
||||
}
|
||||
|
||||
@@ -366,8 +371,12 @@ impl<'writer, 'config> Renderer<'writer, 'config> {
|
||||
write!(self, " ")?;
|
||||
|
||||
let mut previous_label_style = None;
|
||||
for (byte_index, ch) in source
|
||||
.char_indices()
|
||||
let placeholder_metrics = Metrics {
|
||||
byte_index: source.len(),
|
||||
unicode_width: 1,
|
||||
};
|
||||
for (metrics, ch) in self
|
||||
.char_metrics(source.char_indices())
|
||||
// Add a placeholder source column at the end to allow for
|
||||
// printing carets at the end of lines, eg:
|
||||
//
|
||||
@@ -375,13 +384,10 @@ impl<'writer, 'config> Renderer<'writer, 'config> {
|
||||
// 1 │ Hello world!
|
||||
// │ ^
|
||||
// ```
|
||||
//
|
||||
// ' ' is used because its unicode width is equal to 1 according
|
||||
// to `unicode_width::UnicodeWidthChar`.
|
||||
.chain(std::iter::once((source.len(), ' ')))
|
||||
.chain(std::iter::once((placeholder_metrics, '\0')))
|
||||
{
|
||||
// Find the current label style at this column
|
||||
let column_range = byte_index..(byte_index + ch.len_utf8());
|
||||
let column_range = metrics.byte_index..(metrics.byte_index + ch.len_utf8());
|
||||
let current_label_style = single_labels
|
||||
.iter()
|
||||
.filter(|(_, range, _)| is_overlapping(range, &column_range))
|
||||
@@ -402,12 +408,12 @@ impl<'writer, 'config> Renderer<'writer, 'config> {
|
||||
Some(LabelStyle::Primary) => Some(self.chars().single_primary_caret),
|
||||
Some(LabelStyle::Secondary) => Some(self.chars().single_secondary_caret),
|
||||
// Only print padding if we are before the end of the last single line caret
|
||||
None if byte_index < max_label_end => Some(' '),
|
||||
None if metrics.byte_index < max_label_end => Some(' '),
|
||||
None => None,
|
||||
};
|
||||
if let Some(caret_ch) = caret_ch {
|
||||
// FIXME: improve rendering for carets that occurring within character boundaries
|
||||
for _ in 0..self.config.width(ch) {
|
||||
for _ in 0..metrics.unicode_width {
|
||||
write!(self, "{}", caret_ch)?;
|
||||
}
|
||||
}
|
||||
@@ -615,12 +621,39 @@ impl<'writer, 'config> Renderer<'writer, 'config> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Adds tab-stop aware unicode-width computations to an iterator over
|
||||
/// character indices. Assumes that the character indices begin at the start
|
||||
/// of the line.
|
||||
fn char_metrics(
|
||||
&self,
|
||||
char_indices: impl Iterator<Item = (usize, char)>,
|
||||
) -> impl Iterator<Item = (Metrics, char)> {
|
||||
use unicode_width::UnicodeWidthChar;
|
||||
|
||||
let tab_width = self.config.tab_width;
|
||||
let mut unicode_column = 0;
|
||||
|
||||
char_indices.map(move |(byte_index, ch)| {
|
||||
let metrics = Metrics {
|
||||
byte_index,
|
||||
unicode_width: match (ch, tab_width) {
|
||||
('\t', 0) => 0, // Guard divide-by-zero
|
||||
('\t', _) => tab_width - (unicode_column % tab_width),
|
||||
(ch, _) => ch.width().unwrap_or(0),
|
||||
},
|
||||
};
|
||||
unicode_column += metrics.unicode_width;
|
||||
|
||||
(metrics, ch)
|
||||
})
|
||||
}
|
||||
|
||||
/// Location focus.
|
||||
fn snippet_locus(&mut self, locus: &Locus) -> io::Result<()> {
|
||||
write!(
|
||||
self,
|
||||
"{origin}:{line_number}:{column_number}",
|
||||
origin = locus.name,
|
||||
"{name}:{line_number}:{column_number}",
|
||||
name = locus.name,
|
||||
line_number = locus.location.line_number,
|
||||
column_number = locus.location.column_number,
|
||||
)
|
||||
@@ -628,7 +661,7 @@ impl<'writer, 'config> Renderer<'writer, 'config> {
|
||||
|
||||
/// The outer gutter of a source line.
|
||||
fn outer_gutter(&mut self, outer_padding: usize) -> io::Result<()> {
|
||||
write!(self, "{space: >width$}", space = "", width = outer_padding,)?;
|
||||
write!(self, "{space: >width$}", space = "", width = outer_padding)?;
|
||||
write!(self, " ")?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -672,27 +705,25 @@ impl<'writer, 'config> Renderer<'writer, 'config> {
|
||||
trailing_label: Option<(usize, &SingleLabel<'_>)>,
|
||||
char_indices: impl Iterator<Item = (usize, char)>,
|
||||
) -> io::Result<()> {
|
||||
for (byte_index, ch) in char_indices {
|
||||
let column_range = byte_index..(byte_index + ch.len_utf8());
|
||||
for (metrics, ch) in self.char_metrics(char_indices) {
|
||||
let column_range = metrics.byte_index..(metrics.byte_index + ch.len_utf8());
|
||||
let label_style = hanging_labels(single_labels, trailing_label)
|
||||
.filter(|(_, range, _)| column_range.contains(&range.start))
|
||||
.map(|(label_style, _, _)| *label_style)
|
||||
.max_by_key(label_priority_key);
|
||||
|
||||
let spaces = match label_style {
|
||||
None => 0..self.config.width(ch),
|
||||
let mut spaces = match label_style {
|
||||
None => 0..metrics.unicode_width,
|
||||
Some(label_style) => {
|
||||
self.set_color(self.styles().label(severity, label_style))?;
|
||||
write!(self, "{}", self.chars().pointer_left)?;
|
||||
self.reset()?;
|
||||
1..self.config.width(ch)
|
||||
1..metrics.unicode_width
|
||||
}
|
||||
};
|
||||
// Only print padding if we are before the end of the last single line caret
|
||||
if byte_index <= max_label_start {
|
||||
for _ in spaces {
|
||||
write!(self, " ")?;
|
||||
}
|
||||
if metrics.byte_index <= max_label_start {
|
||||
spaces.try_for_each(|_| write!(self, " "))?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -773,12 +804,12 @@ impl<'writer, 'config> Renderer<'writer, 'config> {
|
||||
) -> io::Result<()> {
|
||||
self.set_color(self.styles().label(severity, label_style))?;
|
||||
|
||||
for (_, ch) in source
|
||||
.char_indices()
|
||||
.take_while(|(byte_index, _)| *byte_index < range.end + 1)
|
||||
for (metrics, _) in self
|
||||
.char_metrics(source.char_indices())
|
||||
.take_while(|(metrics, _)| metrics.byte_index < range.end + 1)
|
||||
{
|
||||
// FIXME: improve rendering for carets that occurring within character boundaries
|
||||
for _ in 0..self.config.width(ch) {
|
||||
for _ in 0..metrics.unicode_width {
|
||||
write!(self, "{}", self.chars().multi_top)?;
|
||||
}
|
||||
}
|
||||
@@ -808,12 +839,12 @@ impl<'writer, 'config> Renderer<'writer, 'config> {
|
||||
) -> io::Result<()> {
|
||||
self.set_color(self.styles().label(severity, label_style))?;
|
||||
|
||||
for (_, ch) in source
|
||||
.char_indices()
|
||||
.take_while(|(byte_index, _)| *byte_index < range.end)
|
||||
for (metrics, _) in self
|
||||
.char_metrics(source.char_indices())
|
||||
.take_while(|(metrics, _)| metrics.byte_index < range.end)
|
||||
{
|
||||
// FIXME: improve rendering for carets that occurring within character boundaries
|
||||
for _ in 0..self.config.width(ch) {
|
||||
for _ in 0..metrics.unicode_width {
|
||||
write!(self, "{}", self.chars().multi_bottom)?;
|
||||
}
|
||||
}
|
||||
@@ -915,6 +946,11 @@ impl<'writer, 'config> WriteColor for Renderer<'writer, 'config> {
|
||||
}
|
||||
}
|
||||
|
||||
struct Metrics {
|
||||
byte_index: usize,
|
||||
unicode_width: usize,
|
||||
}
|
||||
|
||||
/// Check if two ranges overlap
|
||||
fn is_overlapping(range0: &Range<usize>, range1: &Range<usize>) -> bool {
|
||||
let start = std::cmp::max(range0.start, range1.start);
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
---
|
||||
source: codespan-reporting/tests/term.rs
|
||||
expression: TEST_DATA.emit_no_color(&config)
|
||||
---
|
||||
warning: tab test
|
||||
┌─ tab_columns:1:2
|
||||
│
|
||||
1 │ hello
|
||||
│ ^^^^^
|
||||
2 │ ∙ hello
|
||||
│ ^^^^^
|
||||
3 │ ∙∙ hello
|
||||
│ ^^^^^
|
||||
4 │ ∙∙∙ hello
|
||||
│ ^^^^^
|
||||
5 │ ∙∙∙∙ hello
|
||||
│ ^^^^^
|
||||
6 │ ∙∙∙∙∙ hello
|
||||
│ ^^^^^
|
||||
7 │ ∙∙∙∙∙∙ hello
|
||||
│ ^^^^^
|
||||
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
---
|
||||
source: codespan-reporting/tests/term.rs
|
||||
expression: TEST_DATA.emit_no_color(&config)
|
||||
---
|
||||
warning: tab test
|
||||
┌─ tab_columns:1:2
|
||||
│
|
||||
1 │ hello
|
||||
│ ^^^^^
|
||||
2 │ ∙ hello
|
||||
│ ^^^^^
|
||||
3 │ ∙∙ hello
|
||||
│ ^^^^^
|
||||
4 │ ∙∙∙ hello
|
||||
│ ^^^^^
|
||||
5 │ ∙∙∙∙ hello
|
||||
│ ^^^^^
|
||||
6 │ ∙∙∙∙∙ hello
|
||||
│ ^^^^^
|
||||
7 │ ∙∙∙∙∙∙ hello
|
||||
│ ^^^^^
|
||||
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
---
|
||||
source: codespan-reporting/tests/term.rs
|
||||
expression: TEST_DATA.emit_no_color(&config)
|
||||
---
|
||||
warning: tab test
|
||||
┌─ tab_columns:1:2
|
||||
│
|
||||
1 │ hello
|
||||
│ ^^^^^
|
||||
2 │ ∙ hello
|
||||
│ ^^^^^
|
||||
3 │ ∙∙ hello
|
||||
│ ^^^^^
|
||||
4 │ ∙∙∙ hello
|
||||
│ ^^^^^
|
||||
5 │ ∙∙∙∙ hello
|
||||
│ ^^^^^
|
||||
6 │ ∙∙∙∙∙ hello
|
||||
│ ^^^^^
|
||||
7 │ ∙∙∙∙∙∙ hello
|
||||
│ ^^^^^
|
||||
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
---
|
||||
source: codespan-reporting/tests/term.rs
|
||||
expression: TEST_DATA.emit_no_color(&config)
|
||||
---
|
||||
warning: tab test
|
||||
┌─ tab_columns:1:2
|
||||
│
|
||||
1 │ hello
|
||||
│ ^^^^^
|
||||
2 │ ∙ hello
|
||||
│ ^^^^^
|
||||
3 │ ∙∙ hello
|
||||
│ ^^^^^
|
||||
4 │ ∙∙∙ hello
|
||||
│ ^^^^^
|
||||
5 │ ∙∙∙∙ hello
|
||||
│ ^^^^^
|
||||
6 │ ∙∙∙∙∙ hello
|
||||
│ ^^^^^
|
||||
7 │ ∙∙∙∙∙∙ hello
|
||||
│ ^^^^^
|
||||
|
||||
|
||||
@@ -11,8 +11,8 @@ warning: unknown weapon `DogJaw`
|
||||
warning: unknown condition `attack-cooldown`
|
||||
┌─ tabbed:4:23
|
||||
│
|
||||
4 │ ReloadingCondition: attack-cooldown
|
||||
│ ^^^^^^^^^^^^^^^ the condition
|
||||
4 │ ReloadingCondition: attack-cooldown
|
||||
│ ^^^^^^^^^^^^^^^ the condition
|
||||
|
||||
warning: unknown field `Foo`
|
||||
┌─ tabbed:5:2
|
||||
|
||||
@@ -11,8 +11,8 @@ warning: unknown weapon `DogJaw`
|
||||
warning: unknown condition `attack-cooldown`
|
||||
┌─ tabbed:4:23
|
||||
│
|
||||
4 │ ReloadingCondition: attack-cooldown
|
||||
│ ^^^^^^^^^^^^^^^ the condition
|
||||
4 │ ReloadingCondition: attack-cooldown
|
||||
│ ^^^^^^^^^^^^^^^ the condition
|
||||
|
||||
warning: unknown field `Foo`
|
||||
┌─ tabbed:5:2
|
||||
|
||||
@@ -11,8 +11,8 @@ warning: unknown weapon `DogJaw`
|
||||
warning: unknown condition `attack-cooldown`
|
||||
┌─ tabbed:4:23
|
||||
│
|
||||
4 │ ReloadingCondition: attack-cooldown
|
||||
│ ^^^^^^^^^^^^^^^ the condition
|
||||
4 │ ReloadingCondition: attack-cooldown
|
||||
│ ^^^^^^^^^^^^^^^ the condition
|
||||
|
||||
warning: unknown field `Foo`
|
||||
┌─ tabbed:5:2
|
||||
|
||||
@@ -631,10 +631,83 @@ mod tabbed {
|
||||
fn tab_width_default_no_color() {
|
||||
let config = TEST_CONFIG.clone();
|
||||
|
||||
insta::assert_snapshot!(
|
||||
"tab_width_default_no_color",
|
||||
TEST_DATA.emit_no_color(&config)
|
||||
);
|
||||
insta::assert_snapshot!(TEST_DATA.emit_no_color(&config));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tab_width_3_no_color() {
|
||||
let config = Config {
|
||||
tab_width: 3,
|
||||
..TEST_CONFIG.clone()
|
||||
};
|
||||
|
||||
insta::assert_snapshot!(TEST_DATA.emit_no_color(&config));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tab_width_6_no_color() {
|
||||
let config = Config {
|
||||
tab_width: 6,
|
||||
..TEST_CONFIG.clone()
|
||||
};
|
||||
|
||||
insta::assert_snapshot!(TEST_DATA.emit_no_color(&config));
|
||||
}
|
||||
}
|
||||
|
||||
mod tab_columns {
|
||||
use super::*;
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref TEST_DATA: TestData<'static, SimpleFiles<&'static str, String>> = {
|
||||
let mut files = SimpleFiles::new();
|
||||
|
||||
let source = [
|
||||
"\thello",
|
||||
"∙\thello",
|
||||
"∙∙\thello",
|
||||
"∙∙∙\thello",
|
||||
"∙∙∙∙\thello",
|
||||
"∙∙∙∙∙\thello",
|
||||
"∙∙∙∙∙∙\thello",
|
||||
].join("\n");
|
||||
let hello_ranges = source
|
||||
.match_indices("hello")
|
||||
.map(|(start, hello)| start..(start+hello.len()))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let file_id = files.add("tab_columns", source);
|
||||
|
||||
let diagnostics = vec![
|
||||
Diagnostic::warning()
|
||||
.with_message("tab test")
|
||||
.with_labels(
|
||||
hello_ranges
|
||||
.into_iter()
|
||||
.map(|range| Label::primary(file_id, range))
|
||||
.collect(),
|
||||
),
|
||||
];
|
||||
|
||||
TestData { files, diagnostics }
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tab_width_default_no_color() {
|
||||
let config = TEST_CONFIG.clone();
|
||||
|
||||
insta::assert_snapshot!(TEST_DATA.emit_no_color(&config));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tab_width_2_no_color() {
|
||||
let config = Config {
|
||||
tab_width: 2,
|
||||
..TEST_CONFIG.clone()
|
||||
};
|
||||
|
||||
insta::assert_snapshot!(TEST_DATA.emit_no_color(&config));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user