subwrite/lib/highlighter.dart

132 lines
4.5 KiB
Dart

import 'package:flutter/material.dart';
import 'package:subwrite/sourcelink.dart';
import 'package:subwrite/theme.dart';
import 'annotation.dart';
import 'line_info.dart';
class LineDecoration {
int start = 0;
int end = 0;
Color color = Colors.white;
Color background = Colors.white;
bool underline = false;
bool italic = false;
bool bold = false;
SourceLink? message;
}
class Highlighter {
Highlighter() {}
List<InlineSpan> run(BuildContext context, String text, int line, bool highlightRisk, TextStyle style, List<LineAnnotation>? insights, int? cursorStart, int? cursorEnd, bool lineSelected,
int maxCharsBeforeWrapping) {
TextStyle defaultStyle = style.copyWith(fontFamily: 'Iosevka', fontSize: fontSize, color: foreground, height: 1.2);
List<InlineSpan> res = <InlineSpan>[];
List<LineDecoration> decors = <LineDecoration>[];
insights
?.where(
(element) => element.annotationType == AnnotationType.highlight,
)
.forEach((element) {
LineDecoration d = LineDecoration();
//print("${element.sourceLink.colStart} ${element.sourceLink.colEnd}");
d.start = element.sourceLink.colStart! - 1;
d.end = element.sourceLink.colEnd! - 2;
d.background = Colors.transparent;
switch (element.type) {
case "header":
d.color = header;
break;
case "bold":
d.color = keyword;
d.bold = true;
break;
case "code":
d.color = function;
break;
case "spellcheck":
d.color = variable;
d.underline = true;
}
decors.add(d);
});
// Assume that decorators do not overlap...
decors.sort((a, b) {
return a.start.compareTo(b.start);
});
text += ' ';
for (int i = 0; i < text.length; i++) {
String current = text[i];
TextStyle style = defaultStyle.copyWith();
// decorate
for (var d in decors) {
// if we are in this decoration...
if (i >= d.start && i <= d.end) {
style = style.copyWith(color: d.color);
if (d.bold = true) {
style = style.copyWith(fontWeight: FontWeight.bold);
}
if (d.underline = true) {
style = style.copyWith(fontStyle: FontStyle.italic);
}
}
}
// is within selection
if (lineSelected) {
style = style.copyWith(backgroundColor: selection);
} else if (cursorStart != null && cursorEnd != null) {
if (i >= cursorStart && i < cursorEnd) {
style = style.copyWith(backgroundColor: selection);
}
} else if (cursorStart != null && cursorEnd == null) {
if (i >= cursorStart) {
style = style.copyWith(backgroundColor: selection);
}
} else if (cursorStart == null && cursorEnd != null) {
if (i < cursorEnd) {
style = style.copyWith(backgroundColor: selection);
}
}
if (highlightRisk) {
style = style.copyWith(
backgroundColor: risk.withOpacity(0.50),
);
}
if (current == " ") {
// NOTE: This is really annoying
// Ideally we could put a non breaking space here and force richtext to just do the thing we want and fill all available space...
// however richtext really doesn't want to do that...
// if this is a space of any kind (even full-width non-breaking spaces), richtext will invisibly drop the space...
// the space happens if this is a size block.
// HOWEVER: if this is an printable char other than a space the algorithm works as expected....EXCEPT
// if we try to change the color to hide that fact the renderer freaks out on lines with more than ~87-90 words and will cause
// the line to shake...
// so we first add the space...
res.add(TextSpan(text: ' \u200B', style: style, mouseCursor: SystemMouseCursors.text));
// and then if we are going to drop the space...
if (i % maxCharsBeforeWrapping == 0) {
// add another space...that we definitely won't drop...
res.add(TextSpan(text: '_\u200B', style: style.copyWith(color: style.backgroundColor), mouseCursor: SystemMouseCursors.text));
}
} else {
// we need to add a '\ufeff' (Zero Width No-Break Space) here to avoid RichText doing ridiculous
// breaking logic with hyphens / slashes etc.
res.add(TextSpan(text: '$current\u200B', style: style, mouseCursor: SystemMouseCursors.text));
}
}
return res;
}
}