Drafter.Widget.CodeView (drafter v0.3.2)

Copy Markdown View Source

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 line
  • Page 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/0 of source code to display. Default ""
  • :path - file path to load. Default nil. The file is read at mount and again whenever the path or :hex_view changes, 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. Default false. Mount-only: neither update/2 nor 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. Default false
  • :height - read only by preferred_height/2, never by mount/1. Default 20

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.

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

t()

@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

component_tag()

@spec component_tag() :: :code_view

The registry tag for this widget.

iex> Drafter.Widget.CodeView.component_tag()
:code_view

focused(state)

from_component_opts(args, opts)

@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}

handle_drag(x, y, state)

@spec handle_drag(integer(), integer(), t()) :: {:ok, t()}

Consumes the drag event and returns {:ok, state} unchanged. Dragging does not pan the document.

handle_event(event, state)

Callback implementation for Drafter.Widget.handle_event/2.

handle_key(arg1, state)

@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

handle_scroll(atom, state)

@spec handle_scroll(:up | :down, t()) :: {:ok, t()}

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}

mount(props)

@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}

preferred_height(args, opts)

@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

render(state, rect)

@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.

unmount(state)

Callback implementation for Drafter.Widget.unmount/1.

update(props, state)

@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}

update_props_from_mount(mount_props, existing_state, opts)

@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.