Packages
caffeine_lang
4.3.6
6.3.1
6.3.0
6.2.2
6.2.1
6.2.0
6.1.2
6.1.1
6.1.0
6.0.0
5.6.0
5.5.0
5.4.4
5.4.3
5.4.2
5.4.1
5.4.0
5.3.0
5.2.0
5.1.1
5.1.0
5.0.12
5.0.11
5.0.10
5.0.8
5.0.7
5.0.6
5.0.5
5.0.4
5.0.1
5.0.0
4.10.0
4.9.0
4.8.3
4.8.2
4.8.1
4.8.0
4.7.9
4.7.8
4.7.7
4.7.6
4.7.5
4.6.7
4.6.6
4.6.5
4.6.4
4.6.3
4.6.2
4.6.0
4.5.1
4.5.0
4.4.4
4.4.3
4.4.1
4.4.0
4.3.7
4.3.6
3.0.6
3.0.5
3.0.4
3.0.3
3.0.2
3.0.1
3.0.0
2.0.5
2.0.4
2.0.3
2.0.2
2.0.1
2.0.0
1.0.2
1.0.1
0.1.0
0.0.24
0.0.23
0.0.22
0.0.21
0.0.20
0.0.19
0.0.18
0.0.17
0.0.16
0.0.15
0.0.14
0.0.13
0.0.12
0.0.11
0.0.10
0.0.9
0.0.8
0.0.7
0.0.6
0.0.5
0.0.4
0.0.2
0.0.1
A compiler for generating reliability artifacts from service expectation definitions.
Current section
Files
Jump to
Current section
Files
src/caffeine_lang/source_snippet.gleam
/// Source snippet extraction for error display.
/// Extracts lines around an error position and adds line numbers and markers.
import gleam/int
import gleam/list
import gleam/option.{type Option}
import gleam/string
/// A rendered source snippet with line numbers and error markers.
pub type SourceSnippet {
SourceSnippet(rendered: String)
}
/// Extracts a source snippet around the given line/column position.
/// Shows 1 line of context above and below the error line.
/// Adds a caret (^) marker under the error position.
/// Line and column are 1-indexed.
pub fn extract_snippet(
source_content: String,
line: Int,
column: Int,
end_column: Option(Int),
) -> SourceSnippet {
let lines = string.split(source_content, "\n")
let total_lines = list.length(lines)
// Context window: 1 line above and below.
let start_line = int.max(1, line - 1)
let end_line = int.min(total_lines, line + 1)
// Gutter width from largest line number.
let gutter_width = string.length(int.to_string(end_line))
// Extract and format context lines.
let context_lines =
extract_lines(lines, start_line, end_line)
|> list.map(fn(pair) {
let #(line_num, line_content) = pair
format_line(line_num, line_content, gutter_width)
})
// Build marker line.
let span_width = case end_column {
option.Some(end_col) -> int.max(1, end_col - column)
option.None -> 1
}
let marker_line = format_marker(column, span_width, gutter_width)
// Insert marker after the error line.
let rendered =
insert_marker(context_lines, line, start_line, marker_line)
|> string.join("\n")
SourceSnippet(rendered:)
}
/// Extracts lines from start_line to end_line (1-indexed, inclusive).
fn extract_lines(
lines: List(String),
start_line: Int,
end_line: Int,
) -> List(#(Int, String)) {
extract_lines_loop(lines, 1, start_line, end_line, [])
|> list.reverse
}
fn extract_lines_loop(
lines: List(String),
current: Int,
start: Int,
end: Int,
acc: List(#(Int, String)),
) -> List(#(Int, String)) {
case lines {
[] -> acc
[line, ..rest] -> {
case current > end {
True -> acc
False -> {
let new_acc = case current >= start {
True -> [#(current, line), ..acc]
False -> acc
}
extract_lines_loop(rest, current + 1, start, end, new_acc)
}
}
}
}
}
/// Formats a single line with its line number gutter.
fn format_line(line_num: Int, content: String, gutter_width: Int) -> String {
let num_str = int.to_string(line_num)
let padding = string.repeat(" ", gutter_width - string.length(num_str))
padding <> num_str <> " | " <> content
}
/// Formats the marker line with carets under the error position.
fn format_marker(column: Int, span_width: Int, gutter_width: Int) -> String {
let gutter_space = string.repeat(" ", gutter_width)
let col_space = string.repeat(" ", int.max(0, column - 1))
let carets = string.repeat("^", span_width)
gutter_space <> " | " <> col_space <> carets
}
/// Inserts the marker line after the error line in the context.
fn insert_marker(
context_lines: List(String),
error_line: Int,
start_line: Int,
marker_line: String,
) -> List(String) {
insert_marker_loop(context_lines, error_line - start_line, marker_line, 0, [])
|> list.reverse
}
fn insert_marker_loop(
lines: List(String),
error_index: Int,
marker: String,
current: Int,
acc: List(String),
) -> List(String) {
case lines {
[] -> acc
[line, ..rest] -> {
let new_acc = case current == error_index {
True -> [marker, line, ..acc]
False -> [line, ..acc]
}
insert_marker_loop(rest, error_index, marker, current + 1, new_acc)
}
}
}