test(coverage): lift os/code-block/deeplink for gate headroom

The three worst-covered files were genuinely untested, not edge cases:
os.dart 27%→~85% (inject the process runner so openURL/reveal don't spawn a
real browser), clide_code_block 39%→~90% (expose the byte→char span mapper
as a top-level fn + render tests), deeplink 29%→~75% (the confirm-opens and
not-activated paths). Buys buffer above the 95% floor so a feature batch
doesn't immediately trip the gate. 95.01% → 95.22%.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-01 22:01:08 +02:00
co-authored by Claude Opus 4.8
parent 1d9000988d
commit 1e8ee2c412
5 changed files with 207 additions and 46 deletions
+8 -3
View File
@@ -15,11 +15,16 @@ class OsLifecycleEvent extends ClideEvent {
String get kind => _kind;
}
/// Runs an external command — injected so [OsBridge] is testable without
/// spawning a real `xdg-open` / `open` / `explorer`.
typedef OsProcessRunner = Future<ProcessResult> Function(String executable, List<String> arguments);
class OsBridge {
OsBridge({required Logger log, required DaemonBus events}) : _log = log, _events = events;
OsBridge({required Logger log, required DaemonBus events, OsProcessRunner? run}) : _log = log, _events = events, _run = run ?? Process.run;
final Logger _log;
final DaemonBus _events;
final OsProcessRunner _run;
Future<bool> openURL(String url) async {
final cmd = _openCommand();
@@ -28,7 +33,7 @@ class OsBridge {
return false;
}
try {
final r = await Process.run(cmd[0], [...cmd.skip(1), url]);
final r = await _run(cmd[0], [...cmd.skip(1), url]);
return r.exitCode == 0;
} catch (e) {
_log.warn('os', 'openURL failed', error: e);
@@ -40,7 +45,7 @@ class OsBridge {
final cmd = _revealCommand(path);
if (cmd == null) return false;
try {
final r = await Process.run(cmd[0], cmd.skip(1).toList());
final r = await _run(cmd[0], cmd.skip(1).toList());
return r.exitCode == 0;
} catch (e) {
_log.warn('os', 'reveal failed', error: e);
+47 -43
View File
@@ -64,7 +64,7 @@ class _ClideCodeBlockState extends State<ClideCodeBlock> {
if (spans == null || spans.isEmpty) {
textSpan = TextSpan(text: widget.source, style: style);
} else {
textSpan = _buildHighlightedSpan(widget.source, spans, style, tokens);
textSpan = buildHighlightedSpan(widget.source, spans, style, tokens);
}
return Container(
@@ -78,53 +78,57 @@ class _ClideCodeBlockState extends State<ClideCodeBlock> {
child: SingleChildScrollView(scrollDirection: Axis.horizontal, child: Text.rich(textSpan)),
);
}
}
static TextSpan _buildHighlightedSpan(String source, List<SyntaxSpan> spans, TextStyle base, dynamic tokens) {
final bytes = utf8.encode(source);
final byteToChar = List<int>.filled(bytes.length + 1, source.length);
var bi = 0;
for (var ci = 0; ci < source.length; ci++) {
byteToChar[bi] = ci;
final rune = source.codeUnitAt(ci);
if (rune < 0x80) {
bi += 1;
} else if (rune < 0x800) {
bi += 2;
} else if (rune >= 0xD800 && rune <= 0xDBFF) {
bi += 4;
ci++;
} else {
bi += 3;
}
/// Maps tree-sitter byte-offset [spans] onto character ranges of [source]
/// (handling multi-byte UTF-8 + surrogate pairs) and colours each run. Exposed
/// so the byte→char mapping and span clipping are unit-tested directly.
@visibleForTesting
TextSpan buildHighlightedSpan(String source, List<SyntaxSpan> spans, TextStyle base, dynamic tokens) {
final bytes = utf8.encode(source);
final byteToChar = List<int>.filled(bytes.length + 1, source.length);
var bi = 0;
for (var ci = 0; ci < source.length; ci++) {
byteToChar[bi] = ci;
final rune = source.codeUnitAt(ci);
if (rune < 0x80) {
bi += 1;
} else if (rune < 0x800) {
bi += 2;
} else if (rune >= 0xD800 && rune <= 0xDBFF) {
bi += 4;
ci++;
} else {
bi += 3;
}
byteToChar[bi] = source.length;
}
byteToChar[bi] = source.length;
final sorted = List.of(spans)..sort((a, b) => a.start.compareTo(b.start));
final children = <TextSpan>[];
var lastChar = 0;
final sorted = List.of(spans)..sort((a, b) => a.start.compareTo(b.start));
final children = <TextSpan>[];
var lastChar = 0;
for (final span in sorted) {
final sChar = span.start < byteToChar.length ? byteToChar[span.start] : source.length;
final eChar = span.end < byteToChar.length ? byteToChar[span.end] : source.length;
final clippedStart = sChar < lastChar ? lastChar : sChar;
if (clippedStart > lastChar) {
children.add(TextSpan(text: source.substring(lastChar, clippedStart)));
}
if (eChar > clippedStart) {
final color = TreeSitterService.colorForRole(span.role, tokens);
children.add(
TextSpan(
text: source.substring(clippedStart, eChar),
style: base.copyWith(color: color),
),
);
}
if (eChar > lastChar) lastChar = eChar;
for (final span in sorted) {
final sChar = span.start < byteToChar.length ? byteToChar[span.start] : source.length;
final eChar = span.end < byteToChar.length ? byteToChar[span.end] : source.length;
final clippedStart = sChar < lastChar ? lastChar : sChar;
if (clippedStart > lastChar) {
children.add(TextSpan(text: source.substring(lastChar, clippedStart)));
}
if (lastChar < source.length) {
children.add(TextSpan(text: source.substring(lastChar)));
if (eChar > clippedStart) {
final color = TreeSitterService.colorForRole(span.role, tokens);
children.add(
TextSpan(
text: source.substring(clippedStart, eChar),
style: base.copyWith(color: color),
),
);
}
if (eChar > lastChar) lastChar = eChar;
}
if (lastChar < source.length) {
children.add(TextSpan(text: source.substring(lastChar)));
}
return TextSpan(style: base, children: children);
}
return TextSpan(style: base, children: children);
}