using Gee; using Gdk; using Gtk; using Pango; namespace Dino.Ui.ConversationSummary { public class CodeBlockPart : Object { public bool is_code; public string text; public string language; public int source_start_char; public CodeBlockPart.from_text(string text, int source_start_char) { this.text = text; this.source_start_char = source_start_char; this.language = ""; } public CodeBlockPart.from_code(string text, string language) { this.is_code = true; this.text = text; this.language = language; } } public class CodeBlockParser : Object { private static Regex? fence_regex; public static ArrayList parse(string text, out bool has_blocks) { var parts = new ArrayList(); Regex regex = get_fence_regex(); MatchInfo match_info; int offset = 0; has_blocks = false; try { regex.match_full(text, -1, offset, 0, out match_info); } catch (RegexError error) { parts.add(new CodeBlockPart.from_text(text, 0)); return parts; } while (match_info.matches()) { int match_start; int match_end; int code_start; int code_end; match_info.fetch_pos(0, out match_start, out match_end); match_info.fetch_pos(2, out code_start, out code_end); int text_end = match_start; if (text_end > offset && text[text_end - 1] == '\n') { text_end--; if (text_end > offset && text[text_end - 1] == '\r') text_end--; } if (text_end > offset) { parts.add(new CodeBlockPart.from_text(text.substring(offset, text_end - offset), text.substring(0, offset).char_count())); } if (code_end > code_start && text[code_end - 1] == '\n') { code_end--; if (code_end > code_start && text[code_end - 1] == '\r') code_end--; } string code = text.substring(code_start, code_end - code_start).replace("\r\n", "\n"); parts.add(new CodeBlockPart.from_code(code, match_info.fetch(1).strip())); has_blocks = true; offset = match_end; if (offset >= text.length) break; try { regex.match_full(text, -1, offset, 0, out match_info); } catch (RegexError error) { break; } } if (offset < text.length) { parts.add(new CodeBlockPart.from_text(text.substring(offset), text.substring(0, offset).char_count())); } return parts; } private static Regex get_fence_regex() { if (fence_regex == null) { try { fence_regex = new Regex("(?ms)^```[\\t ]*([^\\r\\n`]*)[\\t ]*\\r?\\n(.*?)^```[\\t ]*(?:\\r?\\n|$)", RegexCompileFlags.OPTIMIZE); } catch (RegexError error) { GLib.error("Invalid code block expression: %s", error.message); } } return fence_regex; } } public class CodeBlockWidget : Box { private string code; private CodeSyntax.LanguageDefinition? definition; private Label code_label; private ulong realize_id; public CodeBlockWidget(string code, string language) { Object(orientation: Orientation.VERTICAL, spacing: 0, halign: Align.START, overflow: Overflow.HIDDEN); this.code = code; definition = CodeSyntax.find(language); add_css_class("code-block"); var header = new Box(Orientation.HORIZONTAL, 8); header.add_css_class("code-block-header"); string language_name = definition != null ? definition.name : language.strip(); if (language_name == "") language_name = _("Plain text"); var language_label = new Label(language_name) { xalign=0, hexpand=true }; language_label.add_css_class("code-block-language"); header.append(language_label); int line_count = code == "" ? 0 : code.split("\n").length; var line_label = new Label(n("%d line", "%d lines", line_count).printf(line_count)); line_label.add_css_class("code-block-meta"); header.append(line_label); var download_button = new Button.from_icon_name("dino-file-download-symbolic") { tooltip_text=_("Download code") }; download_button.add_css_class("flat"); download_button.add_css_class("code-block-download"); download_button.clicked.connect(save); header.append(download_button); append(header); code_label = new Label(code) { use_markup=false, selectable=true, xalign=0, yalign=0, wrap=false }; code_label.add_css_class("message-text"); code_label.add_css_class("code-block-content"); append(code_label); apply_highlighting(); realize_id = realize.connect(apply_highlighting); } private void apply_highlighting() { var attributes = new AttrList(); if (definition != null) { foreach (CodeSyntax.HighlightSpan span in definition.highlight(code)) { RGBA color = get_token_color(span.style); Attribute foreground = Pango.attr_foreground_new( (uint16) Math.round(color.red * uint16.MAX), (uint16) Math.round(color.green * uint16.MAX), (uint16) Math.round(color.blue * uint16.MAX) ); foreground.start_index = span.start; foreground.end_index = span.end; attributes.insert(foreground.copy()); if (color.alpha < 1) { Attribute alpha = Pango.attr_foreground_alpha_new((uint16) Math.round(color.alpha * uint16.MAX)); alpha.start_index = span.start; alpha.end_index = span.end; attributes.insert(alpha.copy()); } } } code_label.attributes = attributes; } private RGBA get_token_color(CodeSyntax.TokenStyle style) { StyleContext context = code_label.get_style_context(); RGBA color = context.get_color(); string? color_name = null; switch (style) { case CodeSyntax.TokenStyle.COMMENT: color.alpha = 0.62f; break; case CodeSyntax.TokenStyle.CONSTANT: color_name = "error_color"; break; case CodeSyntax.TokenStyle.NUMBER: case CodeSyntax.TokenStyle.PREPROCESSOR: color_name = "warning_color"; break; case CodeSyntax.TokenStyle.STRING: case CodeSyntax.TokenStyle.TYPE: color_name = "success_color"; break; case CodeSyntax.TokenStyle.FUNCTION: case CodeSyntax.TokenStyle.KEYWORD: color_name = "accent_color"; break; } if (color_name != null) { RGBA theme_color; if (context.lookup_color(color_name, out theme_color)) color = theme_color; } return color; } private void save() { var dialog = new FileChooserNative(_("Save code as…"), get_root() as Gtk.Window, FileChooserAction.SAVE, _("Save"), _("Cancel")); dialog.modal = true; dialog.set_current_name("code." + (definition != null ? definition.extension : "txt")); dialog.response.connect((response_id) => { if (response_id == ResponseType.ACCEPT) { File? target = dialog.get_file(); if (target != null) { try { string? etag; target.replace_contents(code.data, null, false, FileCreateFlags.REPLACE_DESTINATION, out etag); } catch (Error error) { warning("Failed to save code: %s", error.message); } } } dialog.destroy(); }); dialog.show(); } public override void dispose() { if (realize_id != 0) { disconnect(realize_id); realize_id = 0; } base.dispose(); } } }