Skip to main content

src/spanner.erl

-module(spanner).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/spanner.gleam").
-export([start/0, between/2, advance/2, label/2]).
-export_type([position/0, span/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.

?MODULEDOC(" Spanner — source code position and span tracking.\n").

-type position() :: {position, integer(), integer(), integer()}.

-type span() :: {span, position(), position()}.

-file("src/spanner.gleam", 16).
?DOC(" Returns the initial position in a source file: line 1, column 1, byte offset 0.\n").
-spec start() -> position().
start() ->
    {position, 1, 1, 0}.

-file("src/spanner.gleam", 21).
?DOC(" Creates a span between two positions.\n").
-spec between(position(), position()) -> span().
between(Start, End) ->
    {span, Start, End}.

-file("src/spanner.gleam", 28).
?DOC(
    " Advances a position in a source string.\n"
    " Handles `\\r\\n` and `\\r` line endings. Columns count graphemes, so\n"
    " multi-byte characters like `é` advance the column by 1.\n"
).
-spec advance(position(), binary()) -> position().
advance(Position, Source) ->
    Normalized = begin
        _pipe = Source,
        _pipe@1 = gleam@string:replace(_pipe, <<"\r\n"/utf8>>, <<"\n"/utf8>>),
        gleam@string:replace(_pipe@1, <<"\r"/utf8>>, <<"\n"/utf8>>)
    end,
    Lines = gleam@string:split(Normalized, <<"\n"/utf8>>),
    case Lines of
        [_] ->
            {position,
                erlang:element(2, Position),
                erlang:element(3, Position) + string:length(Normalized),
                erlang:element(4, Position) + erlang:byte_size(Source)};

        _ ->
            Last_line@1 = case gleam@list:last(Lines) of
                {ok, Last_line} -> Last_line;
                _assert_fail ->
                    erlang:error(#{gleam_error => let_assert,
                                message => <<"Pattern match failed, no pattern matched the value."/utf8>>,
                                file => <<?FILEPATH/utf8>>,
                                module => <<"spanner"/utf8>>,
                                function => <<"advance"/utf8>>,
                                line => 43,
                                value => _assert_fail,
                                start => 1191,
                                'end' => 1234,
                                pattern_start => 1202,
                                pattern_end => 1215})
            end,
            {position,
                (erlang:element(2, Position) + erlang:length(Lines)) - 1,
                string:length(Last_line@1) + 1,
                erlang:element(4, Position) + erlang:byte_size(Source)}
    end.

-file("src/spanner.gleam", 54).
?DOC(" Formats a position with a filename, e.g. `\"src/main.gl:3:5\"`.\n").
-spec label(binary(), span()) -> binary().
label(Filename, Span) ->
    <<<<<<<<Filename/binary, ":"/utf8>>/binary,
                (erlang:integer_to_binary(
                    erlang:element(2, erlang:element(2, Span))
                ))/binary>>/binary,
            ":"/utf8>>/binary,
        (erlang:integer_to_binary(erlang:element(3, erlang:element(2, Span))))/binary>>.