Repository: brendanzab/codespan (rust)

Issue:
Support collapsing multi line output
Love the library, but there's an issue when using it with long multi-line spans--it shows *every* line!

I believe the lines that would need to be changed are these: https://github.com/brendanzab/codespan/blob/e2d6fc3bf504e4e9560bd898cdb5f2f740926505/codespan-reporting/src/term/views/source_snippet/mod.rs#L259-L261

![image](https://user-images.githubusercontent.com/3190756/65933210-8c18aa80-e3c5-11e9-9a50-521b37ba0bf5.png)

rustc, on the other hand, will collapse multiple line spans:

https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=0d4c7f47b06cefb15528f7106766d5f7

![image](https://user-images.githubusercontent.com/3190756/65933307-e0238f00-e3c5-11e9-97c6-930e20f50be6.png)

Does codespan support this?

Pull request:
limit lines of context
This should resolve #125.

The context is limited to 3 lines after the first line or before the last line respectively. The number of lines should probably be configurable; I am still looking into that.

Does this pull request have any chance of being accepted at this time, or is this issue still on hold? @brendanzab mentioned that this should be implemented after another issue (which is now closed).

Patch:
diff --git a/codespan-lsp/Cargo.toml b/codespan-lsp/Cargo.toml
index 85277e9..c477705 100644
--- a/codespan-lsp/Cargo.toml
+++ b/codespan-lsp/Cargo.toml
@@ -10,7 +10,7 @@ documentation = "https://docs.rs/codespan-lsp"
 edition = "2018"
 
 [dependencies]
-codespan = { version = "0.9.5", path = "../codespan" }
+codespan-reporting = { version = "0.9.5", path = "../codespan-reporting" }
 # WARNING: Be extremely careful when expanding this version range.
 # We should be confident that all of the uses of `lsp-types` in `codespan-lsp`
 # will be valid for all the versions in this range. Getting this range wrong
diff --git a/codespan-lsp/src/lib.rs b/codespan-lsp/src/lib.rs
index 05b76fd..26f3937 100644
--- a/codespan-lsp/src/lib.rs
+++ b/codespan-lsp/src/lib.rs
@@ -1,47 +1,97 @@
 //! Utilities for translating from codespan types into Language Server Protocol (LSP) types
 
-use codespan::{
-    ByteIndex, ByteOffset, ColumnIndex, FileId, Files, LineIndex, LineIndexOutOfBoundsError,
-    LocationError, RawIndex, RawOffset, Span, SpanOutOfBoundsError,
-};
+use std::{error, fmt, ops::Range};
+
+use codespan_reporting::files::Files;
+
 // WARNING: Be extremely careful when adding new imports here, as it could break
 // the compatible version range that we claim in our `Cargo.toml`. This could
 // potentially break down-stream builds on a `cargo update`. This is an
 // absolute no-no, breaking much of what we enjoy about Cargo!
 use lsp_types::{Position as LspPosition, Range as LspRange};
-use std::ffi::OsString;
-use std::path::PathBuf;
-use std::{error, fmt};
 
 #[derive(Debug, PartialEq)]
 pub enum Error {
-    UnableToCorrelateFilename(OsString),
-    ColumnOutOfBounds {
-        given: ColumnIndex,
-        max: ColumnIndex,
-    },
+    ColumnOutOfBounds { given: usize, max: usize },
     Location(LocationError),
     LineIndexOutOfBounds(LineIndexOutOfBoundsError),
     SpanOutOfBounds(SpanOutOfBoundsError),
+    MissingFile,
 }
 
 impl fmt::Display for Error {
     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
         match self {
-            Error::UnableToCorrelateFilename(s) => {
-                let p = PathBuf::from(s);
-                write!(f, "Unable to correlate filename `{}` to url", p.display())
-            }
             Error::ColumnOutOfBounds { given, max } => {
                 write!(f, "Column out of bounds - given: {}, max: {}", given, max)
             }
             Error::Location(e) => e.fmt(f),
             Error::LineIndexOutOfBounds(e) => e.fmt(f),
             Error::SpanOutOfBounds(e) => e.fmt(f),
+            Error::MissingFile => write!(f, "File does not exit"),
         }
     }
 }
 
+#[derive(Debug, PartialEq)]
+pub struct LineIndexOutOfBoundsError {
+    pub given: usize,
+    pub max: usize,
+}
+
+impl error::Error for LineIndexOutOfBoundsError {}
+
+impl fmt::Display for LineIndexOutOfBoundsError {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(
+            f,
+            "Line index out of bounds - given: {}, max: {}",
+            self.given, self.max
+        )
+    }
+}
+
+#[derive(Debug, PartialEq)]
+pub enum LocationError {
+    OutOfBounds { given: usize, span: Range<usize> },
+    InvalidCharBoundary { given: usize },
+}
+
+impl error::Error for LocationError {}
+
+impl fmt::Display for LocationError {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match self {
+            LocationError::OutOfBounds { given, span } => write!(
+                f,
+                "Byte index out of bounds - given: {}, span: {}..{}",
+                given, span.start, span.end
+            ),
+            LocationError::InvalidCharBoundary { given } => {
+                write!(f, "Byte index within character boundary - given: {}", given)
+            }
+        }
+    }
+}
+
+#[derive(Debug, PartialEq)]
+pub struct SpanOutOfBoundsError {
+    pub given: Range<usize>,
+    pub span: Range<usize>,
+}
+
+impl error::Error for SpanOutOfBoundsError {}
+
+impl fmt::Display for SpanOutOfBoundsError {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(
+            f,
+            "Span out of bounds - given: {}..{}, span: {}..{}",
+            self.given.start, self.given.end, self.span.start, self.span.end
+        )
+    }
+}
+
 impl From<LocationError> for Error {
     fn from(e: LocationError) -> Error {
         Error::Location(e)
@@ -63,7 +113,7 @@ impl From<SpanOutOfBoundsError> for Error {
 impl error::Error for Error {
     fn source(&self) -> Option<&(dyn error::Error + 'static)> {
         match self {
-            Error::UnableToCorrelateFilename(_) | Error::ColumnOutOfBounds { .. } => None,
+            Error::ColumnOutOfBounds { .. } | Error::MissingFile => None,
             Error::Location(error) => Some(error),
             Error::LineIndexOutOfBounds(error) => Some(error),
             Error::SpanOutOfBounds(error) => Some(error),
@@ -73,61 +123,82 @@ impl error::Error for Error {
 
 fn location_to_position(
     line_str: &str,
-    line: LineIndex,
-    column: ColumnIndex,
-    byte_index: ByteIndex,
+    line: usize,
+    column: usize,
+    byte_index: usize,
 ) -> Result<LspPosition, Error> {
-    if column.to_usize() > line_str.len() {
-        let max = ColumnIndex(line_str.len() as RawIndex);
+    if column > line_str.len() {
+        let max = line_str.len();
         let given = column;
 
         Err(Error::ColumnOutOfBounds { given, max })
-    } else if !line_str.is_char_boundary(column.to_usize()) {
+    } else if !line_str.is_char_boundary(column) {
         let given = byte_index;
 
         Err(LocationError::InvalidCharBoundary { given }.into())
     } else {
-        let line_utf16 = line_str[..column.to_usize()].encode_utf16();
+        let line_utf16 = line_str[..column].encode_utf16();
         let character = line_utf16.count() as u64;
-        let line = line.to_usize() as u64;
+        let line = line as u64;
 
         Ok(LspPosition { line, character })
     }
 }
 
-pub fn byte_index_to_position<Source: AsRef<str>>(
-    files: &Files<Source>,
-    file_id: FileId,
-    byte_index: ByteIndex,
-) -> Result<LspPosition, Error> {
-    let location = files.location(file_id, byte_index)?;
-    let line_span = files.line_span(file_id, location.line)?;
-    let line_str = files.source_slice(file_id, line_span)?;
-    let column = ColumnIndex::from((byte_index - line_span.start()).0 as RawIndex);
-
-    location_to_position(line_str, location.line, column, byte_index)
+pub fn byte_index_to_position<'a, F>(
+    files: &'a F,
+    file_id: F::FileId,
+    byte_index: usize,
+) -> Result<LspPosition, Error>
+where
+    F: Files<'a> + ?Sized,
+{
+    let source = files.source(file_id).ok_or_else(|| Error::MissingFile)?;
+    let source = source.as_ref();
+
+    let line_index =
+        files
+            .line_index(file_id, byte_index)
+            .ok_or_else(|| LineIndexOutOfBoundsError {
+                given: byte_index,
+                max: source.lines().count(),
+            })?;
+    let line_span = files.line_range(file_id, line_index).unwrap();
+
+    let line_str = source
+        .get(line_span.clone())
+        .ok_or_else(|| SpanOutOfBoundsError {
+            given: line_span.clone(),
+            span: 0..source.len(),
+        })?;
+    let column = byte_index - line_span.start;
+
+    location_to_position(line_str, line_index, column, byte_index)
 }
 
-pub fn byte_span_to_range<Source: AsRef<str>>(
-    files: &Files<Source>,
-    file_id: FileId,
-    span: Span,
-) -> Result<LspRange, Error> {
+pub fn byte_span_to_range<'a, F>(
+    files: &'a F,
+    file_id: F::FileId,
+    span: Range<usize>,
+) -> Result<LspRange, Error>
+where
+    F: Files<'a> + ?Sized,
+{
     Ok(LspRange {
-        start: byte_index_to_position(files, file_id, span.start())?,
-        end: byte_index_to_position(files, file_id, span.end())?,
+        start: byte_index_to_position(files, file_id, span.start)?,
+        end: byte_index_to_position(files, file_id, span.end)?,
     })
 }
 
-pub fn character_to_line_offset(line: &str, character: u64) -> Result<ByteOffset, Error> {
-    let line_len = ByteOffset::from(line.len() as RawOffset);
+pub fn character_to_line_offset(line: &str, character: u64) -> Result<usize, Error> {
+    let line_len = line.len();
     let mut character_offset = 0;
 
     let mut chars = line.chars();
     while let Some(ch) = chars.next() {
         if character_offset == character {
-            let chars_off = ByteOffset::from_str_len(chars.as_str());
-            let ch_off = ByteOffset::from_char_len(ch);
+            let chars_off = chars.as_str().len();
+            let ch_off = ch.len_utf8();
 
             return Ok(line_len - chars_off - ch_off);
         }
@@ -140,38 +211,45 @@ pub fn character_to_line_offset(line: &str, character: u64) -> Result<ByteOffset
         Ok(line_len)
     } else {
         Err(Error::ColumnOutOfBounds {
-            given: ColumnIndex(character_offset as RawIndex),
-            max: ColumnIndex(line.len() as RawIndex),
+            given: character_offset as usize,
+            max: line.len(),
         })
     }
 }
 
-pub fn position_to_byte_index<Source: AsRef<str>>(
-    files: &Files<Source>,
-    file_id: FileId,
+pub fn position_to_byte_index<'a, F>(
+    files: &'a F,
+    file_id: F::FileId,
     position: &LspPosition,
-) -> Result<ByteIndex, Error> {
-    let line_span = files.line_span(file_id, position.line as RawIndex)?;
-    let source = files.source_slice(file_id, line_span)?;
+) -> Result<usize, Error>
+where
+    F: Files<'a> + ?Sized,
+{
+    let source = files.source(file_id).ok_or_else(|| Error::MissingFile)?;
+    let source = source.as_ref();
+
+    let line_span = files.line_range(file_id, position.line as usize).unwrap();
+
     let byte_offset = character_to_line_offset(source, position.character)?;
 
-    Ok(line_span.start() + byte_offset)
+    Ok(line_span.start + byte_offset)
 }
 
-pub fn range_to_byte_span<Source: AsRef<str>>(
-    files: &Files<Source>,
-    file_id: FileId,
+pub fn range_to_byte_span<'a, F>(
+    files: &'a F,
+    file_id: F::FileId,
     range: &LspRange,
-) -> Result<Span, Error> {
-    Ok(Span::new(
-        position_to_byte_index(files, file_id, &range.start)?,
-        position_to_byte_index(files, file_id, &range.end)?,
-    ))
+) -> Result<Range<usize>, Error>
+where
+    F: Files<'a> + ?Sized,
+{
+    Ok(position_to_byte_index(files, file_id, &range.start)?
+        ..position_to_byte_index(files, file_id, &range.end)?)
 }
 
 #[cfg(test)]
 mod tests {
-    use codespan::Location;
+    use codespan_reporting::files::{Location, SimpleFiles};
 
     use super::*;
 
@@ -182,7 +260,7 @@ let test = 2
 let test1 = ""
 test
 "#;
-        let mut files = Files::new();
+        let mut files = SimpleFiles::new();
         let file_id = files.add("test", text);
         let pos = position_to_byte_index(
             &files,
@@ -193,7 +271,14 @@ test
             },
         )
         .unwrap();
-        assert_eq!(Location::new(3, 2), files.location(file_id, pos).unwrap());
+        assert_eq!(
+            Location {
+                // One-based
+                line_number: 3 + 1,
+                column_number: 2 + 1,
+            },
+            files.location(file_id, pos).unwrap()
+        );
     }
 
     // The protocol specifies that each `character` in position is a UTF-16 character.
@@ -202,7 +287,7 @@ test
 
     #[test]
     fn unicode_get_byte_index() {
-        let mut files = Files::new();
+        let mut files = SimpleFiles::new();
         let file_id = files.add("unicode", UNICODE);
 
         let result = position_to_byte_index(
@@ -213,7 +298,7 @@ test
                 character: 3,
             },
         );
-        assert_eq!(result, Ok(ByteIndex::from(5)));
+        assert_eq!(result, Ok(5));
 
         let result = position_to_byte_index(
             &files,
@@ -223,15 +308,15 @@ test
                 character: 6,
             },
         );
-        assert_eq!(result, Ok(ByteIndex::from(10)));
+        assert_eq!(result, Ok(10));
     }
 
     #[test]
     fn unicode_get_position() {
-        let mut files = Files::new();
+        let mut files = SimpleFiles::new();
         let file_id = files.add("unicode", UNICODE);
 
-        let result = byte_index_to_position(&files, file_id, ByteIndex::from(5));
+        let result = byte_index_to_position(&files, file_id, 5);
         assert_eq!(
             result,
             Ok(LspPosition {
@@ -240,7 +325,7 @@ test
             })
         );
 
-        let result = byte_index_to_position(&files, file_id, ByteIndex::from(10));
+        let result = byte_index_to_position(&files, file_id, 10);
         assert_eq!(
             result,
             Ok(LspPosition {
diff --git a/codespan-reporting/src/files.rs b/codespan-reporting/src/files.rs
index 41691b9..c06c97f 100644
--- a/codespan-reporting/src/files.rs
+++ b/codespan-reporting/src/files.rs
@@ -115,7 +115,7 @@ pub trait Files<'a> {
 /// Returned by [`Files::location`].
 ///
 /// [`Files::location`]: Files::location
-#[derive(Debug, Copy, Clone)]
+#[derive(Debug, Copy, Clone, PartialEq, Eq)]
 pub struct Location {
     /// The user-facing line number.
     pub line_number: usize,
diff --git a/codespan-reporting/src/term/config.rs b/codespan-reporting/src/term/config.rs
index 7b834d7..cb368b3 100644
--- a/codespan-reporting/src/term/config.rs
+++ b/codespan-reporting/src/term/config.rs
@@ -17,6 +17,14 @@ pub struct Config {
     pub styles: Styles,
     /// Characters to use when rendering the diagnostic.
     pub chars: Chars,
+    /// The minimum number of lines to be shown after the line on which a multiline [`Label`] begins.
+    ///
+    /// Defaults to: `3`.
+    pub start_context_lines: usize,
+    /// The minimum number of lines to be shown before the line on which a multiline [`Label`] ends.
+    ///
+    /// Defaults to: `1`.
+    pub end_context_lines: usize,
 }
 
 impl Default for Config {
@@ -26,6 +34,8 @@ impl Default for Config {
             tab_width: 4,
             styles: Styles::default(),
             chars: Chars::default(),
+            start_context_lines: 3,
+            end_context_lines: 1,
         }
     }
 }
diff --git a/codespan-reporting/src/term/renderer.rs b/codespan-reporting/src/term/renderer.rs
index ab5656e..80b4f51 100644
--- a/codespan-reporting/src/term/renderer.rs
+++ b/codespan-reporting/src/term/renderer.rs
@@ -23,7 +23,7 @@ pub type SingleLabel<'diagnostic> = (LabelStyle, Range<usize>, &'diagnostic str)
 
 /// A multi-line label to render.
 ///
-/// Locations are relative to the start of where the source cord is rendered.
+/// Locations are relative to the start of where the source code is rendered.
 pub enum MultiLabel<'diagnostic> {
     /// Left top corner for multi-line labels.
     ///
@@ -127,6 +127,16 @@ impl<'writer, 'config> Renderer<'writer, 'config> {
         &self.config.styles
     }
 
+    // FIXME: Architectural smell - we'd prefer not to pass information from the renderer to the view.
+    pub fn start_context_lines(&self) -> usize {
+        self.config.start_context_lines
+    }
+
+    // FIXME: Architectural smell - we'd prefer not to pass information from the renderer to the view.
+    pub fn end_context_lines(&self) -> usize {
+        self.config.end_context_lines
+    }
+
     /// Diagnostic header, with severity, code, and message.
     ///
     /// ```text
diff --git a/codespan-reporting/src/term/views.rs b/codespan-reporting/src/term/views.rs
index aa79914..a67ce43 100644
--- a/codespan-reporting/src/term/views.rs
+++ b/codespan-reporting/src/term/views.rs
@@ -59,6 +59,8 @@ where
                     number: line_number,
                     single_labels: vec![],
                     multi_labels: vec![],
+                    // This has to be false by default so we know if it must be rendered by another condition already.
+                    must_render: false,
                 })
             }
         }
@@ -69,6 +71,7 @@ where
             // TODO: How do we reuse these allocations?
             single_labels: Vec<SingleLabel<'diagnostic>>,
             multi_labels: Vec<(usize, LabelStyle, MultiLabel<'diagnostic>)>,
+            must_render: bool,
         }
 
         // TODO: Make this data structure external, to allow for allocation reuse
@@ -158,6 +161,9 @@ where
 
                 line.single_labels
                     .insert(index, (label.style, label_range, &label.message));
+
+                // If this line is not rendered, the SingleLabel is not visible.
+                line.must_render = true;
             } else {
                 // Multiple lines
                 //
@@ -178,8 +184,13 @@ where
                 let label_start = label.range.start - start_line_range.start;
                 let prefix_source = &source[start_line_range.start..label.range.start];
 
-                labeled_file
-                    .get_or_insert_line(start_line_index, start_line_range, start_line_number)
+                let start_line = labeled_file.get_or_insert_line(
+                    start_line_index,
+                    start_line_range.clone(),
+                    start_line_number,
+                );
+
+                start_line
                     .multi_labels
                     // TODO: Do this in the `Renderer`?
                     .push(match prefix_source.trim() {
@@ -200,6 +211,9 @@ where
                         _ => (label_index, label.style, MultiLabel::Top(..label_start)),
                     });
 
+                // The first line has to be rendered so the start of the label is visible.
+                start_line.must_render = true;
+
                 // Marked lines
                 //
                 // ```text
@@ -207,17 +221,24 @@ where
                 // 6 │ │     0 _ => "Fizz"
                 // 7 │ │     _ 0 => "Buzz"
                 // ```
-                // TODO(#125): If start line and end line are too far apart, add a source break.
                 for line_index in (start_line_index + 1)..end_line_index {
                     let line_range = files.line_range(label.file_id, line_index).unwrap();
                     let line_number = files.line_number(label.file_id, line_index).unwrap();
 
                     outer_padding = std::cmp::max(outer_padding, count_digits(line_number));
 
-                    labeled_file
-                        .get_or_insert_line(line_index, line_range, line_number)
-                        .multi_labels
+                    let line = labeled_file.get_or_insert_line(line_index, line_range, line_number);
+
+                    line.multi_labels
                         .push((label_index, label.style, MultiLabel::Left));
+
+                    // The line should be rendered to match the configuration of how much context to show.
+                    line.must_render |=
+                        // Is this line part of the context after the start of the label?
+                        line_index - start_line_index <= renderer.start_context_lines()
+                        ||
+                        // Is this line part of the context before the end of the label?
+                        end_line_index - line_index <= renderer.end_context_lines();
                 }
 
                 // Last labeled line
@@ -228,14 +249,20 @@ where
                 // ```
                 let label_end = label.range.end - end_line_range.start;
 
-                labeled_file
-                    .get_or_insert_line(end_line_index, end_line_range, end_line_number)
-                    .multi_labels
-                    .push((
-                        label_index,
-                        label.style,
-                        MultiLabel::Bottom(..label_end, &label.message),
-                    ));
+                let end_line = labeled_file.get_or_insert_line(
+                    end_line_index,
+                    end_line_range,
+                    end_line_number,
+                );
+
+                end_line.multi_labels.push((
+                    label_index,
+                    label.style,
+                    MultiLabel::Bottom(..label_end, &label.message),
+                ));
+
+                // The last line has to be rendered so the end of the label is visible.
+                end_line.must_render = true;
             }
         }
 
@@ -286,8 +313,11 @@ where
                 )?;
             }
 
-            let mut lines = labeled_file.lines.into_iter().peekable();
-            let current_labels = Vec::new();
+            let mut lines = labeled_file
+                .lines
+                .iter()
+                .filter(|(_, line)| line.must_render)
+                .peekable();
 
             while let Some((line_index, line)) = lines.next() {
                 renderer.render_snippet_source(
@@ -303,13 +333,21 @@ where
                 // Check to see if we need to render any intermediate stuff
                 // before rendering the next line.
                 if let Some((next_line_index, _)) = lines.peek() {
-                    match next_line_index.checked_sub(line_index) {
+                    match next_line_index.checked_sub(*line_index) {
                         // Consecutive lines
                         Some(1) => {}
                         // One line between the current line and the next line
                         Some(2) => {
                             // Write a source line
                             let file_id = labeled_file.file_id;
+
+                            // This line was not intended to be rendered initially.
+                            // To render the line right, we have to get back the original labels.
+                            let labels = labeled_file
+                                .lines
+                                .get(&(line_index + 1))
+                                .map_or(&[][..], |line| &line.multi_labels[..]);
+
                             renderer.render_snippet_source(
                                 outer_padding,
                                 files.line_number(file_id, line_index + 1).unwrap(),
@@ -317,7 +355,7 @@ where
                                 self.diagnostic.severity,
                                 &[],
                                 labeled_file.num_multi_labels,
-                                &current_labels,
+                                labels,
                             )?;
                         }
                         // More than one line between the current line and the next line.
@@ -331,7 +369,7 @@ where
                                 outer_padding,
                                 self.diagnostic.severity,
                                 labeled_file.num_multi_labels,
-                                &current_labels,
+                                &line.multi_labels,
                             )?;
                         }
                     }
@@ -350,7 +388,7 @@ where
                     outer_padding,
                     self.diagnostic.severity,
                     labeled_file.num_multi_labels,
-                    &current_labels,
+                    &[],
                 )?;
             }
         }


Test patch:
diff --git a/codespan-reporting/tests/snapshots/term__multiline_omit__rich_no_color.snap b/codespan-reporting/tests/snapshots/term__multiline_omit__rich_no_color.snap
new file mode 100644
index 0000000..bb9363d
--- /dev/null
+++ b/codespan-reporting/tests/snapshots/term__multiline_omit__rich_no_color.snap
@@ -0,0 +1,38 @@
+---
+source: codespan-reporting/tests/term.rs
+expression: TEST_DATA.emit_no_color(&config)
+---
+error[empty_if]: empty elseif block
+   ┌─ empty_if_comments.lua:1:1
+   │    
+ 1 │ ╭   elseif 3 then
+ 2 │ │   
+ 3 │ │ ╭ 
+ 4 │ │ │ 
+ 5 │ │ │ 
+   · │ │
+ 8 │ │ │ 
+ 9 │ │ │ 
+   │ │ ╰' content should be in here
+10 │ │   else
+   │ ╰───^
+
+error[E0308]: mismatched types
+   ┌─ src/lib.rs:2:6
+   │  
+ 2 │       1
+   │ ╭─────^
+ 3 │ │     + 1
+ 4 │ │     + 1
+   · │
+ 7 │ │     +1
+   │ │      - missing whitespace
+ 8 │ │     + 1
+ 9 │ │     + 1
+10 │ │     + 1
+   │ ╰───────^ expected (), found integer
+   │  
+   = note:	expected type `()`
+     	found type `{integer}`
+
+
diff --git a/codespan-reporting/tests/term.rs b/codespan-reporting/tests/term.rs
index ee02372..df1b263 100644
--- a/codespan-reporting/tests/term.rs
+++ b/codespan-reporting/tests/term.rs
@@ -844,3 +844,79 @@ mod unicode_spans {
     test_emit!(rich_no_color);
     test_emit!(short_no_color);
 }
+
+mod multiline_omit {
+    use super::*;
+
+    lazy_static::lazy_static! {
+        static ref TEST_CONFIG: Config = Config {
+            styles: Styles::with_blue(Color::Blue),
+            start_context_lines: 2,
+            end_context_lines: 1,
+            ..Config::default()
+        };
+
+        static ref TEST_DATA: TestData<'static, SimpleFiles<&'static str, String>> = {
+            let mut files = SimpleFiles::new();
+
+            let file_id1 = files.add(
+                "empty_if_comments.lua",
+                [
+                    "elseif 3 then", // primary label starts here
+                    "",              // context line
+                    "",
+                    "",
+                    "",
+                    "",
+                    "",
+                    "",
+                    "",     // context line
+                    "else", // primary label ends here
+                ]
+                .join("\n"),
+            );
+
+            let file_id2 = files.add(
+                "src/lib.rs",
+                [
+                    "fn main() {",
+                    "    1",   // primary label starts here
+                    "    + 1", // context line
+                    "    + 1", // skip
+                    "    + 1", // skip
+                    "    + 1", // skip
+                    "    +1",  // secondary label here
+                    "    + 1", // this single line will not be skipped; the previously filtered out label must be retrieved
+                    "    + 1", // context line
+                    "    + 1", // primary label ends here
+                    "}",
+                ]
+                .join("\n"),
+            );
+
+            let diagnostics = vec![
+                Diagnostic::error()
+                    .with_message("empty elseif block")
+                    .with_code("empty_if")
+                    .with_labels(vec![
+                        Label::primary(file_id1, 0..23),
+                        Label::secondary(file_id1, 15..21).with_message("content should be in here"),
+                    ]),
+                Diagnostic::error()
+                    .with_message("mismatched types")
+                    .with_code("E0308")
+                    .with_labels(vec![
+                        Label::primary(file_id2, 17..80).with_message("expected (), found integer"),
+                        Label::secondary(file_id2, 55..55).with_message("missing whitespace"),
+                    ])
+                    .with_notes(vec![
+                        "note:\texpected type `()`\n\tfound type `{integer}`".to_owned()
+                    ]),
+            ];
+
+            TestData { files, diagnostics }
+        };
+    }
+
+    test_emit!(rich_no_color);
+}