Our sender chip is our name, turned over, with the star on the page to its right. A mention of us has the turned-over chip and no star: the message that names us gets the star at the left of its time, so every time moves two cells right, and the Messages list puts it at the left of the row's age. A mention chip drops its @ where chips are drawn. The compose line draws each complete @[Name] as its chip and treats it as one atom: the arrows step over it, and Backspace or Delete removes all of it. A reply inserts its mention at the cursor, so one message can name several nodes. The prompt and the typed text are white. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
142 lines
5.4 KiB
Python
142 lines
5.4 KiB
Python
# SPDX-License-Identifier: Apache-2.0
|
||
"""Tests for the compose line of a chat: each complete mention is one atom, drawn as a chip."""
|
||
|
||
from __future__ import annotations
|
||
|
||
from rich.style import Style
|
||
from rich.text import Text
|
||
|
||
from meshterm.ui.compose import MentionEditor
|
||
from meshterm.ui.pathline import PATH_INK, POWERLINE_SEP, PathHop
|
||
from meshterm.ui.widgets import mention_hop
|
||
|
||
|
||
def _hop(name: str) -> PathHop:
|
||
"""The hop of a mention with no key: the keyless grey."""
|
||
return mention_hop(name, None)
|
||
|
||
|
||
def _style(text: Text, run: str) -> Style:
|
||
"""The parsed style of the span over ``run`` (not the stamp of the path line)."""
|
||
return Style.parse(
|
||
next(
|
||
str(s.style)
|
||
for s in text.spans
|
||
if text.plain[s.start : s.end] == run and str(s.style) != PATH_INK
|
||
)
|
||
)
|
||
|
||
|
||
def test_backspace_after_a_mention_removes_all_of_it() -> None:
|
||
"""One Backspace removes the whole token, and the cursor goes where the token was."""
|
||
editor = MentionEditor("hi @[Bob]")
|
||
assert editor.edit("backspace")
|
||
assert (editor.text, editor.cursor) == ("hi ", 3)
|
||
editor.edit("backspace") # an ordinary character: one at a time, as before
|
||
assert editor.text == "hi"
|
||
|
||
|
||
def test_delete_before_a_mention_removes_all_of_it() -> None:
|
||
"""Delete at the start of a token removes the whole token."""
|
||
editor = MentionEditor("@[Bob] hi")
|
||
editor.edit("home")
|
||
editor.edit("delete")
|
||
assert (editor.text, editor.cursor) == (" hi", 0)
|
||
|
||
|
||
def test_left_and_right_go_over_a_mention_in_one_step() -> None:
|
||
"""The cursor is never inside a token."""
|
||
editor = MentionEditor("a @[Bob] b")
|
||
editor.edit("left") # before "b"
|
||
editor.edit("left") # before the space
|
||
assert editor.cursor == 8
|
||
editor.edit("left") # over the token
|
||
assert editor.cursor == 2
|
||
editor.edit("right") # over it again
|
||
assert editor.cursor == 8
|
||
|
||
|
||
def test_a_word_jump_that_stops_inside_a_mention_goes_out_of_it() -> None:
|
||
"""A name can have a space. ^← goes to the start of the token, ^→ to its end."""
|
||
editor = MentionEditor("@[Bob Smith] hi")
|
||
editor.edit("end")
|
||
editor.edit("ctrl_left") # to "hi"
|
||
editor.edit("ctrl_left") # into "Smith]", then out to the start
|
||
assert editor.cursor == 0
|
||
editor.edit("ctrl_right") # to the space in the name, then out to the end
|
||
assert editor.cursor == len("@[Bob Smith]")
|
||
|
||
|
||
def test_a_typed_bracket_completes_a_mention_with_the_cursor_after_it() -> None:
|
||
"""The ``]`` makes the token an atom, and the cursor is at its end."""
|
||
editor = MentionEditor()
|
||
for ch in "hey @[Al":
|
||
editor.edit("text", ch)
|
||
assert editor.atoms() == [] # not complete yet: each character is its own
|
||
editor.edit("text", "]")
|
||
assert editor.atoms() == [(4, 9, "Al")] and editor.cursor == 9
|
||
|
||
|
||
def test_an_at_typed_in_front_of_a_bracketed_name_puts_the_cursor_after_it() -> None:
|
||
"""An edit that completes a token around the cursor moves the cursor out of the token."""
|
||
editor = MentionEditor("[Bob]")
|
||
editor.edit("home")
|
||
editor.edit("text", "@")
|
||
assert editor.atoms() == [(0, 6, "Bob")] and editor.cursor == 6
|
||
|
||
|
||
def test_an_open_token_typed_in_front_of_a_mention_does_not_join_it() -> None:
|
||
"""``@[`` before a chip leaves the chip as it is, and the cursor stays where it was."""
|
||
editor = MentionEditor("@[Bob]")
|
||
editor.edit("home")
|
||
editor.edit("text", "@")
|
||
editor.edit("text", "[")
|
||
assert editor.atoms() == [(2, 8, "Bob")] and editor.cursor == 2
|
||
|
||
|
||
def test_insert_puts_text_at_the_cursor() -> None:
|
||
"""A mention goes in where the cursor is, not at the start of the line."""
|
||
editor = MentionEditor("hi there")
|
||
editor.cursor = 3
|
||
editor.insert("@[Bob]")
|
||
assert editor.text == "hi @[Bob] there" and editor.cursor == 9
|
||
|
||
|
||
def test_the_line_draws_each_mention_as_a_chip(powerline) -> None:
|
||
"""The token shows as the chip of the node, with no ``@`` and no brackets."""
|
||
powerline(True)
|
||
editor = MentionEditor("hi @[Bob] ok")
|
||
line = editor.render_line(_hop)
|
||
assert "@[" not in line.plain and "@" not in line.plain
|
||
assert f"{POWERLINE_SEP} Bob " in line.plain
|
||
assert line.plain.startswith("› hi ")
|
||
assert any(str(s.style) == PATH_INK for s in line.spans) # the wrap keeps it whole
|
||
|
||
|
||
def test_the_prompt_and_the_text_are_white(powerline) -> None:
|
||
"""The ``›`` has the ``cursor`` white, and the typed text has the ``compose`` white."""
|
||
powerline(True)
|
||
line = MentionEditor("hi").render_line(_hop)
|
||
styles = {line.plain[s.start : s.end]: str(s.style) for s in line.spans}
|
||
assert styles["› "] == "cursor"
|
||
assert styles["h"] == "compose" and styles["i"] == "compose"
|
||
|
||
|
||
def test_the_chip_under_the_cursor_is_the_white_slot(powerline) -> None:
|
||
"""With the cursor at the start of a token, the chip is the white chip of a slot."""
|
||
powerline(True)
|
||
editor = MentionEditor("@[Bob]")
|
||
editor.edit("home")
|
||
style = _style(editor.render_line(_hop), "Bob")
|
||
assert style.bgcolor.name == "#ffffff"
|
||
|
||
|
||
def test_a_chip_past_the_byte_limit_is_red(powerline) -> None:
|
||
"""A token that goes past the limit has the error colour, the same as each character."""
|
||
powerline(True)
|
||
editor = MentionEditor("hi @[Bob]")
|
||
editor.edit("home") # the cursor away from the chip
|
||
normal = _style(editor.render_line(_hop), "Bob")
|
||
over = _style(editor.render_line(_hop, overflow_at=5), "Bob")
|
||
assert over.color != normal.color and over.bgcolor != normal.bgcolor
|