Renders a scrollable, syntax-highlighted code viewer with keyboard and drag navigation.
Source text is provided directly via :source or loaded from disk via :path.
When :path is given the file is read at mount time and the language is inferred
from the extension when possible. Syntax highlighting is performed by the
tree-sitter CLI when available, falling back to the built-in Elixir
highlighter for .ex and .exs files.
Keyboard controls (when focused):
↑/↓— scroll one linePage Up/Page Down— scroll ten lines←/→— horizontal scroll by five columns, the right limit being the longest line less 20 columns- Mouse wheel — vertical scroll by three lines
Every other key is consumed and leaves the state unchanged, so nothing bubbles out of a focused code view. Dragging is declared but does nothing.
Component tag
Tag :code_view, built by Drafter.App as either {:code_view, opts} or
{:code_view, source, opts}:
code_view(opts)
code_view(source, opts)from_component_opts/2 ignores the positional argument and reads every prop
from opts, so pass the text as source: rather than positionally.
Options
:source-String.t/0of source code to display. Default"":path- file path to load. Defaultnil. The file is read at mount and again whenever the path or:hex_viewchanges, and an unreadable file yields an empty document. Takes precedence over:source:language- syntax language atom, e.g.:elixir,:exs. Default:text:show_line_numbers-boolean/0, display a line number gutter. Defaultfalse. Mount-only: neitherupdate/2nor a re-render changes it:hex_view-boolean/0, render the content as a hex dump of 16 bytes per row with offset and ASCII columns instead of as source text, suppressing syntax highlighting. Defaultfalse:height- read only bypreferred_height/2, never bymount/1. Default20
update/2 re-reads :path, :hex_view, :source and :language, and reloads
the document only when the path, the hex mode, or a non-empty source actually
changes. A reload resets both scroll offsets to 0.
Widget value
Drafter.get_widget_value/1 is not implemented for this widget and returns nil.
Usage
code_view(source: File.read!("lib/my_app.ex"), language: :elixir, show_line_numbers: true)
code_view(path: "/etc/hosts")
code_view(path: "/bin/ls", hex_view: true)
Summary
Functions
The registry tag for this widget.
Turns the {:code_view, opts} element into a props map for mount/1.
Consumes the drag event and returns {:ok, state} unchanged. Dragging does not
pan the document.
Callback implementation for Drafter.Widget.handle_event/2.
Scrolls the document.
Scrolls the document three lines per wheel notch.
Builds the code view state from props, reading :path from disk when given and
splitting the document into lines.
opts[:height], or 20 when it is absent.
Draws the visible window of the document into rect.
Callback implementation for Drafter.Widget.unmount/1.
Reloads the document when the source changed, and returns state untouched
otherwise.
Narrows the props a re-render feeds to update/2 to :source, :path,
:language and :hex_view. :show_line_numbers is left out and stays as
mounted.
Types
@type t() :: %Drafter.Widget.CodeView{ focused: boolean(), h_scroll_offset: non_neg_integer(), hex_view: boolean(), highlights: term() | nil, language: atom(), lines: [String.t()], path: Path.t() | nil, scroll_offset: non_neg_integer(), show_line_numbers: boolean() }
Functions
@spec component_tag() :: :code_view
The registry tag for this widget.
iex> Drafter.Widget.CodeView.component_tag()
:code_view
@spec from_component_opts( term(), keyword() ) :: Drafter.Widget.props()
Turns the {:code_view, opts} element into a props map for mount/1.
The positional argument is ignored, so the text must be passed as source:.
iex> Drafter.Widget.CodeView.from_component_opts("ignored", source: "a")
%{source: "a", path: nil, language: :text, show_line_numbers: false, hex_view: false}
Consumes the drag event and returns {:ok, state} unchanged. Dragging does not
pan the document.
Callback implementation for Drafter.Widget.handle_event/2.
@spec handle_key(Drafter.Widget.key(), t()) :: {:ok, t()}
Scrolls the document.
:up/:down move one line, :page_up/:page_down move ten, and :left/:right
move five columns. The vertical offset is clamped to 0..length(lines) - 1; the
horizontal offset is clamped to 0..max(0, longest_line - 20). Every other key is
consumed unchanged. Always returns {:ok, state}, so nothing bubbles.
iex> cv = Drafter.Widget.CodeView.mount(%{source: "a\nb\nc"})
iex> {:ok, moved} = Drafter.Widget.CodeView.handle_key(:down, cv)
iex> moved.scroll_offset
1
iex> cv = Drafter.Widget.CodeView.mount(%{source: "a\nb\nc"})
iex> {:ok, paged} = Drafter.Widget.CodeView.handle_key(:page_down, cv)
iex> paged.scroll_offset
2
iex> cv = Drafter.Widget.CodeView.mount(%{source: String.duplicate("x", 40)})
iex> {:ok, right} = Drafter.Widget.CodeView.handle_key(:right, cv)
iex> right.h_scroll_offset
5
iex> cv = Drafter.Widget.CodeView.mount(%{source: "short"})
iex> {:ok, right} = Drafter.Widget.CodeView.handle_key(:right, cv)
iex> right.h_scroll_offset
0
iex> cv = Drafter.Widget.CodeView.mount(%{source: "a"})
iex> Drafter.Widget.CodeView.handle_key(:enter, cv) |> elem(0)
:ok
Scrolls the document three lines per wheel notch.
The offset is clamped to 0..length(lines) - 1, so the last line can always be
scrolled to the top of the rect. Always returns {:ok, state}.
iex> cv = Drafter.Widget.CodeView.mount(%{source: "a\nb\nc\nd\ne\nf"})
iex> {:ok, down} = Drafter.Widget.CodeView.handle_scroll(:down, cv)
iex> {:ok, up} = Drafter.Widget.CodeView.handle_scroll(:up, down)
iex> {down.scroll_offset, up.scroll_offset}
{3, 0}
@spec mount(Drafter.Widget.props()) :: t()
Builds the code view state from props, reading :path from disk when given and
splitting the document into lines.
Both scroll offsets and :focused always start at 0/false. Highlighting is
computed here, so a source with no recognised syntax leaves :highlights as
nil.
iex> cv = Drafter.Widget.CodeView.mount(%{source: "a\nb\nc"})
iex> {cv.lines, cv.language, cv.scroll_offset, cv.show_line_numbers, cv.hex_view}
{["a", "b", "c"], :text, 0, false, false}
iex> cv = Drafter.Widget.CodeView.mount(%{})
iex> {cv.lines, cv.path, cv.h_scroll_offset, cv.focused}
{[""], nil, 0, false}
@spec preferred_height( term(), keyword() ) :: pos_integer()
opts[:height], or 20 when it is absent.
iex> Drafter.Widget.CodeView.preferred_height(nil, [])
20
iex> Drafter.Widget.CodeView.preferred_height(nil, height: 8)
8
@spec render(t(), Drafter.Widget.rect()) :: [Drafter.Draw.Strip.t()]
Draws the visible window of the document into rect.
Emits one strip per visible line, at most rect.height of them starting at
:scroll_offset, so a document shorter than the rect leaves the remaining rows
untouched rather than blanking them. With :show_line_numbers the gutter takes
the width of the largest line number plus one column, and the rest is content
scrolled left by :h_scroll_offset.
Callback implementation for Drafter.Widget.unmount/1.
@spec update(Drafter.Widget.props(), t()) :: t()
Reloads the document when the source changed, and returns state untouched
otherwise.
A reload happens when :path differs from the mounted one, when :hex_view
differs, or when :source is present, non-empty, and differs from the joined
current lines. When it happens both scroll offsets reset to 0 and :language is
re-read; :show_line_numbers never changes.
iex> cv = Drafter.Widget.CodeView.mount(%{source: "a\nb"})
iex> {:ok, scrolled} = Drafter.Widget.CodeView.handle_key(:down, cv)
iex> Drafter.Widget.CodeView.update(%{source: "a\nb"}, scrolled).scroll_offset
1
iex> cv = Drafter.Widget.CodeView.mount(%{source: "a\nb"})
iex> {:ok, scrolled} = Drafter.Widget.CodeView.handle_key(:down, cv)
iex> reloaded = Drafter.Widget.CodeView.update(%{source: "x\ny\nz"}, scrolled)
iex> {reloaded.lines, reloaded.scroll_offset}
{["x", "y", "z"], 0}
@spec update_props_from_mount(Drafter.Widget.props(), t(), keyword()) :: Drafter.Widget.props()
Narrows the props a re-render feeds to update/2 to :source, :path,
:language and :hex_view. :show_line_numbers is left out and stays as
mounted.