-module(caffeine_lang@source_snippet). -compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). -define(FILEPATH, "src/caffeine_lang/source_snippet.gleam"). -export([extract_snippet/4]). -export_type([source_snippet/0]). -if(?OTP_RELEASE >= 27). -define(MODULEDOC(Str), -moduledoc(Str)). -define(DOC(Str), -doc(Str)). -else. -define(MODULEDOC(Str), -compile([])). -define(DOC(Str), -compile([])). -endif. -type source_snippet() :: {source_snippet, binary()}. -file("src/caffeine_lang/source_snippet.gleam", 125). -spec insert_marker_loop( list(binary()), integer(), binary(), integer(), list(binary()) ) -> list(binary()). insert_marker_loop(Lines, Error_index, Marker, Current, Acc) -> case Lines of [] -> Acc; [Line | Rest] -> New_acc = case Current =:= Error_index of true -> [Marker, Line | Acc]; false -> [Line | Acc] end, insert_marker_loop(Rest, Error_index, Marker, Current + 1, New_acc) end. -file("src/caffeine_lang/source_snippet.gleam", 115). ?DOC(" Inserts the marker line after the error line in the context.\n"). -spec insert_marker(list(binary()), integer(), integer(), binary()) -> list(binary()). insert_marker(Context_lines, Error_line, Start_line, Marker_line) -> _pipe = insert_marker_loop( Context_lines, Error_line - Start_line, Marker_line, 0, [] ), lists:reverse(_pipe). -file("src/caffeine_lang/source_snippet.gleam", 107). ?DOC(" Formats the marker line with carets under the error position.\n"). -spec format_marker(integer(), integer(), integer()) -> binary(). format_marker(Column, Span_width, Gutter_width) -> Gutter_space = gleam@string:repeat(<<" "/utf8>>, Gutter_width), Col_space = gleam@string:repeat(<<" "/utf8>>, gleam@int:max(0, Column - 1)), Carets = gleam@string:repeat(<<"^"/utf8>>, Span_width), <<<<<>/binary, Col_space/binary>>/binary, Carets/binary>>. -file("src/caffeine_lang/source_snippet.gleam", 100). ?DOC(" Formats a single line with its line number gutter.\n"). -spec format_line(integer(), binary(), integer()) -> binary(). format_line(Line_num, Content, Gutter_width) -> Num_str = erlang:integer_to_binary(Line_num), Padding = gleam@string:repeat( <<" "/utf8>>, Gutter_width - string:length(Num_str) ), <<<<<>/binary, " | "/utf8>>/binary, Content/binary>>. -file("src/caffeine_lang/source_snippet.gleam", 78). ?DOC(" Collects lines from current_line to end_line (inclusive).\n"). -spec collect_lines(binary(), integer(), integer(), list({integer(), binary()})) -> list({integer(), binary()}). collect_lines(Source, Current_line, End_line, Acc) -> gleam@bool:guard( Current_line > End_line, Acc, fun() -> case gleam@string:split_once(Source, <<"\n"/utf8>>) of {ok, {Line, Rest}} -> collect_lines( Rest, Current_line + 1, End_line, [{Current_line, Line} | Acc] ); {error, _} -> gleam@bool:guard( gleam@string:is_empty(Source), Acc, fun() -> [{Current_line, Source} | Acc] end ) end end ). -file("src/caffeine_lang/source_snippet.gleam", 69). ?DOC(" Skips past the first `count` lines by splitting on newlines.\n"). -spec skip_lines(binary(), integer()) -> binary(). skip_lines(Source, Count) -> gleam@bool:guard( Count =< 0, Source, fun() -> case gleam@string:split_once(Source, <<"\n"/utf8>>) of {ok, {_, Rest}} -> skip_lines(Rest, Count - 1); {error, _} -> Source end end ). -file("src/caffeine_lang/source_snippet.gleam", 58). ?DOC( " Extracts only the lines in [start_line, end_line] without splitting the entire source.\n" " Uses split_once to skip lines before the range and stop after it.\n" ). -spec extract_context_lines(binary(), integer(), integer()) -> list({integer(), binary()}). extract_context_lines(Source, Start_line, End_line) -> Remaining = skip_lines(Source, Start_line - 1), _pipe = collect_lines(Remaining, Start_line, End_line, []), lists:reverse(_pipe). -file("src/caffeine_lang/source_snippet.gleam", 18). ?DOC( " Extracts a source snippet around the given line/column position.\n" " Shows 1 line of context above and below the error line.\n" " Adds a caret (^) marker under the error position.\n" " Line and column are 1-indexed.\n" ). -spec extract_snippet( binary(), integer(), integer(), gleam@option:option(integer()) ) -> source_snippet(). extract_snippet(Source_content, Line, Column, End_column) -> Start_line = gleam@int:max(1, Line - 1), End_line = Line + 1, Context = extract_context_lines(Source_content, Start_line, End_line), Max_line_num = case gleam@list:last(Context) of {ok, {N, _}} -> N; {error, _} -> End_line end, Gutter_width = string:length(erlang:integer_to_binary(Max_line_num)), Context_lines = begin _pipe = Context, gleam@list:map( _pipe, fun(Pair) -> format_line( erlang:element(1, Pair), erlang:element(2, Pair), Gutter_width ) end ) end, Span_width = case End_column of {some, End_col} -> gleam@int:max(1, End_col - Column); none -> 1 end, Marker_line = format_marker(Column, Span_width, Gutter_width), Rendered = begin _pipe@1 = insert_marker(Context_lines, Line, Start_line, Marker_line), gleam@string:join(_pipe@1, <<"\n"/utf8>>) end, {source_snippet, Rendered}.