No task waits on a Codex review; the redesign tasks are confirmed on the product owner's Z Fold7; TASK-016's leftovers are on hold; no biometric reauthentication. Agent: main (opus) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
9.3 KiB
TASK-048: Source code viewer readability (line numbers + syntax highlighting)
Status: implemented; all required checks pass; live-confirmed on the human product owner's device, including a horizontal-scroll regression found and fixed during that same live testing Owner: Claude Reviewer: Codex
Outcome
The human product owner reported the in-app source file viewer was hard to read -- no line numbers, no syntax highlighting, everything rendered as one plain block of monospace text with only horizontal/vertical scroll. Chose (from three offered options) the fullest one: line numbers plus color syntax highlighting. Both, plus a word-wrap toggle, are now live -- reached in three iterations after the first two shipped real regressions that live device testing caught (see Design notes' numbered account).
Scope
- Included: new
feature/codebrowser/content/SyntaxHighlighter.kt(a lightweight, regex-based, per-line highlighter -- keywords/strings/comments/numbers -- for Kotlin, Java, C#, JavaScript/TS, Python, Shell, YAML, XML, JSON, CSS, SQL, with a plain-text fallback for anything else); a newCodeBlockcomposable inCodeBrowserScreen.ktreplacing the single giantTextforContentClassification.Textwith line-numbered, highlighted rows; a 줄바꿈 켜기/끄기 (word wrap on/off) toggle button next to the existing copy button. - Excluded: a real per-language parser/tokenizer (see Design notes on why a regex approach was chosen instead); highlighting for languages outside the list above (falls back to a plain, uncolored monospace line -- still gets line numbers and wrap, just no color).
Design notes
- Regex-based, not a real tokenizer, and explicitly per-line with no cross-line state. A block
comment (
/* ... */) or a string that spans multiple lines is only colored correctly on the line(s) where it actually starts/ends, not on every line in between. A real multi-language parser would be a much larger undertaking than a file viewer needs; this covers the overwhelmingly common single-line case cheaply, in pure Kotlin, with zero new dependencies -- consistent with this codebase's established preference (see the launcher icon's and TASK-047's own notes) for hand-rolled solutions over pulling in a library for one narrow need. - Colors are fixed, not derived from the app's own sky-blue theme. Source highlighting has its
own well-known conventions (blue keywords, orange/red strings, green comments) independent of any
given app's brand color; two hand-picked sets (light/dark, closely following VS Code's own
defaults) are used instead, switching on
isSystemInDarkTheme(). - Word wrap defaults off (matches the previous behavior exactly, so nothing changes unless the
user opts in), toggled per file-view via a plain
OutlinedButton(no icon dependency, matching every other button in this screen). - The row layout went through three shapes, in order, each caught by live on-device testing, not
by build/lint/unit tests, none of which can see a real touch gesture or a real wrapped-text
layout:
- LazyColumn, one Row per line, each row's code
Texton its ownModifier.horizontalScroll(sharedState). Matches typical modern Compose guidance for a virtualized, line-numbered list. Live-tested: horizontal drags did nothing at all. Most likely cause:BranchDetailScreen'sHorizontalPager(Files/History tabs) turns offuserScrollEnabledfor scrollable code, but apparently doesn't fully exit the nested-scroll picture for aLazyColumnspecifically the way it does for a plainModifier.horizontalScrollchain -- see that file's own pre-existing comment on this exact class of gesture conflict, written for the previous (pre-TASK-048) single-Textimplementation. - Two independent Columns side by side (gutter, code) inside one outer
RowplusModifier.verticalScroll, moving offLazyColumnentirely to sidestep (1). Fixed horizontal scroll. Live-tested with word wrap on: line numbers drifted out of sync with their code as soon as any line wrapped to multiple visual rows -- the gutter Column (one fixed-heightTextper logical line) has no way to grow to match a code Column line that just became taller. - Final: one outer
ColumnplusModifier.verticalScroll, gutter and codeTextpaired back into oneRowper logical line (closer to shape 1 again, minusLazyColumn), each row's codeTexton its own independentModifier.horizontalScroll(no longer a sharedScrollStateacross rows -- an acceptable, visually invisible trade-off, since every row's text always starts at column 0 regardless). Live-confirmed: horizontal scroll works, and word-wrap alignment is correct, because Compose sizes each Row to fit its own (possibly multi-row-tall) content and the single gutter number simply sits at that row's top. Not lazy (every line is a real composable, not virtualized) in any of the three shapes -- the original single-Textversion wasn't virtualized either, andSourceContentClassifieralready bounds how many lines a file view ever receives, so this was never a new performance ceiling, only a correctness one.
- LazyColumn, one Row per line, each row's code
Acceptance criteria
- Every source text view shows a line-number gutter.
- Kotlin/Java/C#/JavaScript-TS/Python/Shell/YAML/XML/JSON/CSS/SQL keywords, strings, comments, and numbers render in distinct colors; an unrecognized extension still renders (plain, with line numbers), never crashes or shows nothing.
- A 줄바꿈 켜기/끄기 toggle switches between horizontal-scroll and wrapped rendering.
- With word wrap off, a line longer than the viewport is horizontally scrollable by dragging it.
- With word wrap on, every line's gutter number stays aligned with its own (possibly multi-row-tall) wrapped content.
- Build, unit tests, and lint pass.
- Live-confirmed on the human product owner's device against real C# and XML/
.csprojfiles: keyword/string/comment coloring, line numbers, horizontal drag-scroll (off), and wrap alignment (on) all independently verified after the shape-3 fix.
Required checks
./gradlew compileDebugKotlin compileDebugUnitTestKotlin testDebugUnitTest assembleDebug lintDebug
Evidence
- New
feature/codebrowser/content/SyntaxHighlighter.kt:SyntaxLanguageenum,syntaxLanguageForPath(extension-based),CodeSyntaxColors+rememberCodeSyntaxColors,highlightLine(regex tokenize + classify), per-languageLangSpeckeyword/comment tables. feature/codebrowser/CodeBrowserScreen.kt: newCodeBlockcomposable (final shape: outerColumn+verticalScroll, oneRowper line pairing gutter + codeText, codeTextgetting its ownhorizontalScrollwhen word wrap is off);ContentClassification.Textbranch rewired to it; newwordWrapstate + toggle button.res/values/strings_codebrowser.xml:codebrowser_word_wrap_on/_off../gradlew compileDebugKotlin compileDebugUnitTestKotlin testDebugUnitTest assembleDebug lintDebug: BUILD SUCCESSFUL, all unit tests passing (no behavior touched outside the file viewer), 0 lint errors.- Live device: installed over wireless
adbthree times (once per shape above) as each round of testing surfaced the next issue. Final round: opened a real.csfile (KeyenceBCRConfig.cs) and a real.csproj(XML) file inRobotAutomationDevelopment-Dept/LGIT-LaserCompressionBonding; screenshot-confirmed keyword coloring, string coloring, line numbers, a horizontal swipe actually moving a long line's content, and (with word wrap on) multi-row-wrapped lines keeping correctly-aligned single gutter numbers. - Diagnosed the shape-1 failure via
adb shell uiautomator dump(confirmed the accessibility tree exposed only the top-level scroll container asscrollable="true", not a per-row one -- suggestive though not itself conclusive) and, more directly, by reasoning fromBranchDetailScreen.kt's own pre-existing comment describing the identical Pager-vs-content gesture conflict for the previous implementation.
Known limitations
- No cross-line tokenizer state (see Design notes) -- multi-line block comments/strings only color correctly at their start/end line.
- Highlighting rules are hand-picked keyword sets per language, not sourced from each language's full official grammar -- covers common keywords, not every contextual one.
- Line-number gutter width is a character-count heuristic (
lineNumberWidth * 8 + 12dp), not a measured intrinsic width -- close enough visually, not pixel-exact across every font-scale setting. - The final layout (see two follow-up dev-log entries the same day) branches by word-wrap mode: word wrap off uses two side-by-side Columns (gutter, code) sharing one horizontalScroll region so every line scrolls together; word wrap on pairs gutter+code per Row so number alignment survives wrapped lines. The two-Column shape is only safe because word-wrap-off guarantees every line is exactly one visual row tall -- if that assumption ever stopped holding (e.g. a future change rendering a line as more than one row while word wrap is off), gutter/code alignment would silently break again the same way it did in the abandoned two-Column-always attempt.
Review findings
None yet -- no Codex review pending (Codex retired, 2026-09-29).