feat: issue #32 markdown editor for TUI
This commit is contained in:
@@ -38,14 +38,18 @@ defmodule BDS.TUITest do
|
||||
state
|
||||
end
|
||||
|
||||
defp screen_text(state, width \\ 100, height \\ 32) do
|
||||
defp screen_cells(state, width \\ 100, height \\ 32) do
|
||||
session = CellSession.new(width, height)
|
||||
frame = %ExRatatui.Frame{width: width, height: height}
|
||||
:ok = CellSession.draw(session, BDS.TUI.render(state, frame))
|
||||
snapshot = CellSession.take_cells(session)
|
||||
:ok = CellSession.close(session)
|
||||
|
||||
snapshot.cells
|
||||
end
|
||||
|
||||
defp screen_text(state, width \\ 100, height \\ 32) do
|
||||
state
|
||||
|> screen_cells(width, height)
|
||||
|> Enum.group_by(& &1.row)
|
||||
|> Enum.sort_by(fn {row, _} -> row end)
|
||||
|> Enum.map_join("\n", fn {_row, cells} ->
|
||||
@@ -53,6 +57,22 @@ defmodule BDS.TUITest do
|
||||
end)
|
||||
end
|
||||
|
||||
# The cell where `text` starts on screen (used to assert per-cell styles).
|
||||
defp cell_at_text(cells, text) do
|
||||
{_row, row_cells} =
|
||||
cells
|
||||
|> Enum.group_by(& &1.row)
|
||||
|> Enum.find(fn {_row, row_cells} ->
|
||||
row_cells |> Enum.sort_by(& &1.col) |> Enum.map_join("", & &1.symbol) |> String.contains?(text)
|
||||
end)
|
||||
|
||||
sorted = Enum.sort_by(row_cells, & &1.col)
|
||||
joined = Enum.map_join(sorted, & &1.symbol)
|
||||
{byte_offset, _len} = :binary.match(joined, text)
|
||||
char_offset = joined |> binary_part(0, byte_offset) |> String.length()
|
||||
Enum.at(sorted, char_offset)
|
||||
end
|
||||
|
||||
defp key(code, modifiers), do: %Key{code: code, kind: "press", modifiers: modifiers}
|
||||
|
||||
defp press(state, code, modifiers \\ []) do
|
||||
@@ -68,7 +88,7 @@ defmodule BDS.TUITest do
|
||||
assert text =~ "Hello TUI"
|
||||
end
|
||||
|
||||
test "opening a post lands in the markdown preview, not the editor", %{post: post} do
|
||||
test "opening a post lands in the editor with the raw source", %{post: post} do
|
||||
{:ok, _} =
|
||||
BDS.Posts.update_post(post.id, %{content: "# Big Heading\n\nsome **bold** words"})
|
||||
|
||||
@@ -76,19 +96,66 @@ defmodule BDS.TUITest do
|
||||
|
||||
assert state.focus == :editor
|
||||
assert state.editor.post.id == post.id
|
||||
refute state.editor.preview?
|
||||
|
||||
# The editor shows the raw markdown source.
|
||||
assert screen_text(state) =~ "**bold**"
|
||||
|
||||
# ctrl+e flips to the rendered markdown preview: inline markers are
|
||||
# consumed by the styling.
|
||||
state = press(state, "e", ["ctrl"])
|
||||
assert state.editor.preview?
|
||||
|
||||
text = screen_text(state)
|
||||
assert text =~ "Big Heading"
|
||||
# Rendered markdown: inline markers are consumed by the styling
|
||||
# (headings keep their # but get styled, invisible in a text dump).
|
||||
assert text =~ "some bold words"
|
||||
refute text =~ "**bold**"
|
||||
|
||||
# ctrl+e drops into the editing textarea with the raw source.
|
||||
# and back to the editor.
|
||||
state = press(state, "e", ["ctrl"])
|
||||
refute state.editor.preview?
|
||||
assert ExRatatui.textarea_get_value(state.editor.textarea) =~ "**bold**"
|
||||
assert screen_text(state) =~ "**bold**"
|
||||
end
|
||||
|
||||
test "the editor word-wraps long lines", %{post: post} do
|
||||
long_line = String.duplicate("lorem ipsum dolor sit amet ", 8) <> "FINALWORD"
|
||||
{:ok, _} = BDS.Posts.update_post(post.id, %{content: long_line})
|
||||
|
||||
state = mount!() |> press("enter")
|
||||
refute state.editor.preview?
|
||||
|
||||
# The tail of a long line wraps onto visible rows without any
|
||||
# horizontal scrolling.
|
||||
assert screen_text(state, 80, 32) =~ "FINALWORD"
|
||||
end
|
||||
|
||||
test "the cursor cell stays visible when the cursor sits below the viewport", %{post: post} do
|
||||
content = Enum.map_join(1..30, "\n", &"line #{&1}")
|
||||
{:ok, _} = BDS.Posts.update_post(post.id, %{content: content})
|
||||
|
||||
state = mount!() |> press("enter")
|
||||
|
||||
# The cursor opens at the end of the buffer — source row 29 of 30.
|
||||
assert ExRatatui.textarea_cursor(state.editor.textarea) == {29, 7}
|
||||
|
||||
cells = screen_cells(state)
|
||||
# The cursor cell is the single space carrying the cursor background
|
||||
# inside the editor area (the sidebar selection also uses a cyan bg).
|
||||
assert Enum.any?(cells, &(&1.bg == :cyan and &1.symbol == " " and &1.col >= 34))
|
||||
end
|
||||
|
||||
test "the cursor line has a distinct background from the other lines", %{post: post} do
|
||||
{:ok, _} = BDS.Posts.update_post(post.id, %{content: "first\nsecond\nthird"})
|
||||
|
||||
state = mount!() |> press("enter")
|
||||
|
||||
# Move the cursor from the end of the buffer onto the first line.
|
||||
state = state |> press("up") |> press("up")
|
||||
assert ExRatatui.textarea_cursor(state.editor.textarea) == {0, 5}
|
||||
|
||||
cells = screen_cells(state)
|
||||
assert cell_at_text(cells, "first").bg == :dark_gray
|
||||
refute cell_at_text(cells, "second").bg == :dark_gray
|
||||
end
|
||||
|
||||
test "ctrl+s persists textarea edits", %{post: post} do
|
||||
@@ -126,26 +193,17 @@ defmodule BDS.TUITest do
|
||||
assert state.focus == :sidebar
|
||||
end
|
||||
|
||||
test "the preview word-wraps and ctrl+e toggles back and forth", %{post: post} do
|
||||
test "the preview word-wraps long lines", %{post: post} do
|
||||
long_line = String.duplicate("lorem ipsum dolor sit amet ", 8) <> "FINALWORD"
|
||||
{:ok, _} = BDS.Posts.update_post(post.id, %{content: long_line})
|
||||
|
||||
state = mount!() |> press("enter")
|
||||
state = mount!() |> press("enter") |> press("e", ["ctrl"])
|
||||
assert state.editor.preview?
|
||||
|
||||
# The textarea cannot soft-wrap, so the tail of a long line is off-screen
|
||||
# there; the preview renders through the Markdown widget, which wraps.
|
||||
assert screen_text(state, 80, 32) =~ "FINALWORD"
|
||||
|
||||
state = press(state, "e", ["ctrl"])
|
||||
refute state.editor.preview?
|
||||
|
||||
state = press(state, "e", ["ctrl"])
|
||||
assert state.editor.preview?
|
||||
end
|
||||
|
||||
test "keys in preview mode do not edit the content" do
|
||||
state = mount!() |> press("enter")
|
||||
state = mount!() |> press("enter") |> press("e", ["ctrl"])
|
||||
assert state.editor.preview?
|
||||
before = ExRatatui.textarea_get_value(state.editor.textarea)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user