fortrangoingonforty/fortsh / 67e4cc7

Browse files

render active selection in reverse video

Extend the inlined readline redraw path with a three-segment render
when the selection is live: plain prefix, ESC[7m...ESC[27m for the
selected bytes, plain suffix. Syntax highlighting is skipped for the
whole line while a selection is active — partial-token highlighting
on the out-of-selection segments produced false command/keyword
colors, and most editors drop syntax colors inside the selection
anyway.

Reverse video is the same pattern the prefix-search renderer uses at
readline.f90:1664 — proven across Terminal.app, iTerm2, Ghostty, and
the mainstream Linux terminals. ESC[27m is used over ESC[0m to clear
only reverse video, leaving any other attributes the prompt set
behind untouched.

New local sel_start/sel_end integers are declared at subroutine scope
(not inside a block) to stay compatible with the inlined-redraw
workaround for flang-new's large-derived-type ABI bug. No new
subroutine calls — the whole render lives in the existing redraw
block.

Tests: 4 new assertions in selection.yaml that force
FORTSH_TEST_MODE=0 and match on the literal ESC[7m...ESC[27m pattern
around the selected text. Sprint 1's 15 behavioral tests still pass
(they run in test mode, skipping the full redraw path entirely).
Adjacent specs — line_editing, history, vi_mode, completion,
signals_jobs, prompt_display, stress — all clean: 267 tests total
with zero regressions.
Authored by espadonne
SHA
67e4cc711e621b7264abe6f21fafe8b41fee0ada
Parents
81a91f7
Tree
0d38896

2 changed files

StatusFile+-
M src/io/readline.f90 31 1
M tests/interactive/test_specs/selection.yaml 67 0
src/io/readline.f90modified
@@ -1147,6 +1147,7 @@ contains
11471147
     integer :: suggestion_display_len, available_space
11481148
     integer :: current_col, current_row
11491149
     integer :: highlighted_len  ! Actual length of highlighted string
1150
+    integer :: sel_start, sel_end  ! Selection byte range for Sprint 2 rendering
11501151
     character(len=MAX_LINE_LEN) :: temp_buf  ! For buffer extraction
11511152
     ! Variables for UTF-8 support (moved out of block to avoid flang-new crash)
11521153
     character(len=4) :: utf8_char
@@ -1657,8 +1658,37 @@ contains
16571658
               ! Try syntax highlighting
16581659
               call state_buffer_get(module_input_state, temp_buf)
16591660
 
1661
+              ! Shift-phase selection rendering (Sprint 2): when a selection is
1662
+              ! active, render in three segments — plain prefix, reverse-video
1663
+              ! selection (ESC[7m...ESC[27m), plain suffix. Syntax highlighting
1664
+              ! is skipped for the whole line while a selection is live to avoid
1665
+              ! mis-coloring partial token substrings. Reverse video (#13) is
1666
+              ! preferred over background colors: proven on Terminal.app,
1667
+              ! iTerm2, Ghostty, and the mainstream Linux terminals, and already
1668
+              ! used for prefix search rendering below.
1669
+              if (module_input_state%selection_active) then
1670
+                ! Compute and clamp the byte range.
1671
+                sel_start = min(module_input_state%selection_anchor, module_input_state%cursor_pos)
1672
+                sel_end   = max(module_input_state%selection_anchor, module_input_state%cursor_pos)
1673
+                if (sel_start < 0) sel_start = 0
1674
+                if (sel_end > module_input_state%length) sel_end = module_input_state%length
1675
+                ! Segment 1: plain text from start up to selection start.
1676
+                if (sel_start > 0) then
1677
+                  write(output_unit, '(a)', advance='no') temp_buf(1:sel_start)
1678
+                end if
1679
+                ! Segment 2: selected bytes in reverse video.
1680
+                if (sel_end > sel_start) then
1681
+                  write(output_unit, '(a)', advance='no') char(27) // '[7m'
1682
+                  write(output_unit, '(a)', advance='no') temp_buf(sel_start+1:sel_end)
1683
+                  write(output_unit, '(a)', advance='no') char(27) // '[27m'
1684
+                end if
1685
+                ! Segment 3: plain text from selection end to buffer end.
1686
+                if (sel_end < module_input_state%length) then
1687
+                  write(output_unit, '(a)', advance='no') temp_buf(sel_end+1:module_input_state%length)
1688
+                end if
1689
+
16601690
               ! In prefix search mode, render prefix in reverse video + rest plain
1661
-              if (module_input_state%in_prefix_search .and. &
1691
+              else if (module_input_state%in_prefix_search .and. &
16621692
                   (module_input_state%prefix_search_idx /= 0 .or. module_input_state%prefix_search_flash)) then
16631693
                 ! Prefix in reverse video
16641694
                 write(output_unit, '(a)', advance='no') char(27) // '[7m'
tests/interactive/test_specs/selection.yamlmodified
@@ -271,3 +271,70 @@ tests:
271271
       - send_line: "pwd"
272272
     expect_output: "/"
273273
     match_type: "contains"
274
+
275
+  # =============================================================
276
+  # SPRINT 2 — VISIBLE RENDERING
277
+  # These tests force FORTSH_TEST_MODE off so the full redraw path
278
+  # runs and emits reverse-video ANSI codes (ESC[7m ... ESC[27m)
279
+  # around the selected byte range. Each test uses fresh_session to
280
+  # avoid polluting adjacent tests with redraw noise.
281
+  # =============================================================
282
+
283
+  - name: "Sprint 2: Shift+Left emits reverse-video around selection"
284
+    # After "echo hello world" + 5x Shift+Left, the rendering should
285
+    # include the literal sequence ESC[7m followed by "world" followed
286
+    # by ESC[27m. pexpect matches on regex, so we escape the [.
287
+    env:
288
+      FORTSH_TEST_MODE: "0"
289
+    fresh_session: true
290
+    steps:
291
+      - send: "echo hello world"
292
+      - send_key: "S-Left"
293
+      - send_key: "S-Left"
294
+      - send_key: "S-Left"
295
+      - send_key: "S-Left"
296
+      - send_key: "S-Left"
297
+    expect_output: "\u001b\\[7mworld\u001b\\[27m"
298
+
299
+  - name: "Sprint 2: Shift+Home highlights to start of line"
300
+    # "abcd" + Shift+Home → anchor=4, cursor=0. Render has prefix empty,
301
+    # reverse-video "abcd", suffix empty.
302
+    env:
303
+      FORTSH_TEST_MODE: "0"
304
+    fresh_session: true
305
+    steps:
306
+      - send: "abcd"
307
+      - send_key: "S-Home"
308
+    expect_output: "\u001b\\[7mabcd\u001b\\[27m"
309
+
310
+  - name: "Sprint 2: Ctrl+Shift+Left highlights one word back"
311
+    # "aaa bbb" + C-S-Left → anchor=7, cursor=4. Render has prefix
312
+    # "aaa ", reverse-video "bbb", suffix empty.
313
+    env:
314
+      FORTSH_TEST_MODE: "0"
315
+    fresh_session: true
316
+    steps:
317
+      - send: "aaa bbb"
318
+      - send_key: "C-S-Left"
319
+    expect_output: "\u001b\\[7mbbb\u001b\\[27m"
320
+
321
+  - name: "Sprint 2: plain Left after selection collapses — no ESC[7m in tail"
322
+    # Select, then plain Left (snaps to left edge, collapses). The
323
+    # NEXT redraw must NOT contain reverse video. We verify by sending
324
+    # more chars and expecting them NOT to be wrapped in ESC[7m.
325
+    # Use 'expect_not' to check absence of reverse video on the suffix.
326
+    env:
327
+      FORTSH_TEST_MODE: "0"
328
+    fresh_session: true
329
+    steps:
330
+      - send: "abcd"
331
+      - send_key: "S-Left"
332
+      - send_key: "S-Left"
333
+      - send_key: "Left"
334
+      - send: "Z"
335
+    # After Left, selection collapses (cursor=2, snapped to min edge).
336
+    # Inserting Z goes at pos 2 → "abZcd". The final redraw should show
337
+    # the buffer plain (no reverse video around Z). Assert on presence
338
+    # of "abZcd" text in the stream — if selection incorrectly persisted,
339
+    # the render would wrap bytes in ESC[7m instead.
340
+    expect_output: "abZcd"