From 6114079ecd3f537cc963c3328be89329c99ce315 Mon Sep 17 00:00:00 2001 From: CFFinch62 Date: Thu, 22 Jan 2026 23:31:59 -0500 Subject: [PATCH 1/2] Add markdown-preview plugin --- .../markdown-preview/implementation_plan.md | 73 +++ .../markdown-preview/markdown-preview.plugin | 10 + .../markdown-preview/markdown-preview.vala | 592 ++++++++++++++++++ plugins/markdown-preview/meson.build | 34 + plugins/meson.build | 1 + 5 files changed, 710 insertions(+) create mode 100644 plugins/markdown-preview/implementation_plan.md create mode 100644 plugins/markdown-preview/markdown-preview.plugin create mode 100644 plugins/markdown-preview/markdown-preview.vala create mode 100644 plugins/markdown-preview/meson.build diff --git a/plugins/markdown-preview/implementation_plan.md b/plugins/markdown-preview/implementation_plan.md new file mode 100644 index 0000000000..c1fbf7bbb4 --- /dev/null +++ b/plugins/markdown-preview/implementation_plan.md @@ -0,0 +1,73 @@ +# Markdown Preview Plugin + +Add a plugin to Elementary Code that allows users to preview rendered markdown in a side-by-side view alongside the editor. + +## User Review Required + +> [!IMPORTANT] +> **Rendering Approach**: This plugin will use WebKit (webkit2gtk-4.0) to render the markdown as HTML. The preview will appear in a separate pane that can be toggled on/off. The markdown will be converted to HTML and styled with GitHub-flavored markdown CSS for a clean, familiar appearance. + +> [!IMPORTANT] +> **UI Integration**: The preview will be accessible via: +> - A toolbar button that appears when editing markdown files +> - A keyboard shortcut (Ctrl+Shift+M) +> - The preview pane will appear to the right of the editor in a split view + +## Proposed Changes + +### Markdown Preview Plugin + +A new plugin that provides live markdown rendering capabilities for markdown files. + +#### [NEW] [markdown-preview.vala](file:///home/chuck/Dropbox/Programming/Languages_and_Code/Programming_Projects/Programming_Tools/elementary-code-altdistros/plugins/markdown-preview/markdown-preview.vala) + +Main plugin implementation with: +- WebKit WebView for rendering HTML +- Markdown to HTML conversion using a simple parser +- Live preview updates as the user types (with debouncing) +- Split pane integration with the editor +- Toggle button in toolbar for showing/hiding preview +- Keyboard shortcut (Ctrl+Shift+M) support +- Auto-scroll synchronization between editor and preview +- GitHub-flavored markdown CSS styling + +#### [NEW] [markdown-preview.plugin](file:///home/chuck/Dropbox/Programming/Languages_and_Code/Programming_Projects/Programming_Tools/elementary-code-altdistros/plugins/markdown-preview/markdown-preview.plugin) + +Plugin metadata file describing the plugin name, description, and author information. + +#### [NEW] [meson.build](file:///home/chuck/Dropbox/Programming/Languages_and_Code/Programming_Projects/Programming_Tools/elementary-code-altdistros/plugins/markdown-preview/meson.build) + +Build configuration for the markdown-preview plugin, including webkit2gtk-4.0 dependency. + +--- + +### Plugin Registration + +#### [MODIFY] [meson.build](file:///home/chuck/Dropbox/Programming/Languages_and_Code/Programming_Projects/Programming_Tools/elementary-code-altdistros/plugins/meson.build) + +Add `subdir('markdown-preview')` to register the new plugin in the build system. + +## Verification Plan + +### Automated Tests +- Build the project with `meson setup build --prefix=/usr && ninja -C build` +- Verify no compilation errors +- Check that the plugin is installed to the correct directory + +### Manual Verification +- Launch Elementary Code +- Open a markdown file ([.md](file:///home/chuck/.gemini/antigravity/brain/b88974f6-b6d8-4497-a368-2ef66eb65ce1/task.md) extension) +- Verify the preview toggle button appears in the toolbar +- Click the button or press Ctrl+Shift+M to show the preview pane +- Verify markdown is rendered correctly with: + - Headers (H1-H6) + - Bold and italic text + - Lists (ordered and unordered) + - Code blocks with syntax highlighting + - Links + - Images + - Blockquotes + - Tables +- Edit the markdown and verify the preview updates in real-time +- Verify the preview can be toggled on/off +- Test with non-markdown files to ensure the button doesn't appear diff --git a/plugins/markdown-preview/markdown-preview.plugin b/plugins/markdown-preview/markdown-preview.plugin new file mode 100644 index 0000000000..2e46e6295e --- /dev/null +++ b/plugins/markdown-preview/markdown-preview.plugin @@ -0,0 +1,10 @@ +[Plugin] +Module=markdown-preview +Loader=C +IAge=2 +Name=Markdown Preview +Description=Live preview of rendered Markdown files in a side-by-side view +Icon=view-paged-symbolic +Authors=Elementary Code Contributors +Copyright=Copyright © 2026 Elementary Code Contributors +Hidden=false diff --git a/plugins/markdown-preview/markdown-preview.vala b/plugins/markdown-preview/markdown-preview.vala new file mode 100644 index 0000000000..a621d4c1d3 --- /dev/null +++ b/plugins/markdown-preview/markdown-preview.vala @@ -0,0 +1,592 @@ +// -*- Mode: vala; indent-tabs-mode: nil; tab-width: 4 -*- +/*** + BEGIN LICENSE + + Copyright (C) 2026 Elementary Code Contributors + This program is free software: you can redistribute it and/or modify it + under the terms of the GNU Lesser General Public License version 3, as published + by the Free Software Foundation. + + This program is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranties of + MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR + PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program. If not, see + + END LICENSE +***/ + +public class Code.Plugins.MarkdownPreview : Peas.ExtensionBase, Scratch.Services.ActivatablePlugin { + Scratch.Services.Interface plugins; + Scratch.Widgets.SourceView? current_source = null; + Scratch.HeaderBar? toolbar = null; + Gtk.ToggleButton? preview_button = null; + + // Store preview state per document + private Gee.HashMap preview_states; + + public Object object { owned get; set construct; } + + public void update_state () {} + + private class PreviewState { + public WebKit.WebView web_view; + public Gtk.ScrolledWindow scrolled_window; + public Gtk.Paned? paned = null; + public Gtk.ScrolledWindow? original_scroll = null; + public bool visible = false; + + public PreviewState () { + web_view = new WebKit.WebView (); + web_view.expand = true; + + scrolled_window = new Gtk.ScrolledWindow (null, null); + scrolled_window.add (web_view); + scrolled_window.set_policy (Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC); + + // Handle Ctrl+Scroll for zooming on the WebView directly + web_view.scroll_event.connect ((event) => { + if ((event.state & Gdk.ModifierType.CONTROL_MASK) != 0) { + double zoom = web_view.zoom_level; + + if (event.direction == Gdk.ScrollDirection.UP) { + zoom += 0.1; + } else if (event.direction == Gdk.ScrollDirection.DOWN) { + zoom -= 0.1; + } else if (event.direction == Gdk.ScrollDirection.SMOOTH) { + double dx, dy; + event.get_scroll_deltas (out dx, out dy); + // In smooth scrolling, dy is negative for scrolling up (zooming in) + // and positive for scrolling down (zooming out) + if (dy < 0) zoom += 0.1; + else if (dy > 0) zoom -= 0.1; + } + + // Clamp zoom level logic if needed, but WebKit handles reasonable limits. + // Just ensure it doesn't go negative or too small. + if (zoom < 0.1) zoom = 0.1; + + web_view.zoom_level = zoom; + return true; // Stop propagation (prevent scrolling) + } + return false; + }); + } + } + + public void activate () { + plugins = (Scratch.Services.Interface) object; + preview_states = new Gee.HashMap (); + + // Create toggle button for toolbar + preview_button = new Gtk.ToggleButton (); + preview_button.image = new Gtk.Image.from_icon_name ("view-paged-symbolic", Gtk.IconSize.LARGE_TOOLBAR); + preview_button.tooltip_text = "Toggle Markdown Preview (Ctrl+Shift+M)"; + preview_button.toggled.connect (on_preview_toggled); + preview_button.no_show_all = true; + + // Hook into toolbar + plugins.hook_toolbar.connect ((t) => { + toolbar = t; + }); + + // Hook into document changes + plugins.hook_document.connect ((doc) => { + if (current_source != null) { + current_source.notify["language"].disconnect (on_language_changed); + } + + current_source = doc.source_view; + on_language_changed (); + + current_source.notify["language"].connect (on_language_changed); + + // Update preview button state based on this document's preview state + var state = preview_states.get (doc); + if (state != null) { + preview_button.active = state.visible; + } else { + preview_button.active = false; + } + }); + } + + private void on_language_changed () { + if (toolbar == null || current_source == null) { + return; + } + + var lang = current_source.language; + bool is_markdown = (lang != null && lang.id == "markdown"); + + // Show/hide preview button based on file type + if (is_markdown) { + if (preview_button.parent == null) { + toolbar.pack_end (preview_button); + } + preview_button.show (); + } else { + preview_button.hide (); + + // Hide preview if visible for non-markdown files + if (preview_button.active) { + preview_button.active = false; + } + } + } + + private void on_preview_toggled () { + if (current_source == null) { + return; + } + + // Get the current document + var doc = get_document_for_source (current_source); + if (doc == null) { + return; + } + + if (preview_button.active) { + show_preview (doc); + } else { + hide_preview (doc); + } + } + + private Scratch.Services.Document? get_document_for_source (Scratch.Widgets.SourceView source) { + // The source view's parent chain: SourceView -> ScrolledWindow -> Grid -> Paned -> Grid -> Stack -> Document + var widget = source.parent; + while (widget != null) { + if (widget is Scratch.Services.Document) { + return (Scratch.Services.Document) widget; + } + widget = widget.parent; + } + return null; + } + + private void show_preview (Scratch.Services.Document doc) { + var state = preview_states.get (doc); + if (state == null) { + state = new PreviewState (); + preview_states.set (doc, state); + } + + if (state.visible) { + return; // Already showing + } + + // Find the scrolled window containing the source view + var scroll = doc.source_view.parent as Gtk.ScrolledWindow; + if (scroll == null) { + warning ("Could not find scroll window for source view"); + return; + } + + // Get the parent of the scroll window (should be a Grid) + var scroll_parent = scroll.parent; + if (scroll_parent == null) { + warning ("Could not find parent of scroll window"); + return; + } + + // Store reference to original scroll window + state.original_scroll = scroll; + + // Create a paned widget to hold both the source view and preview + state.paned = new Gtk.Paned (Gtk.Orientation.HORIZONTAL); + + // Remove the scroll window from its parent + scroll_parent.remove (scroll); + + // Add both to the paned widget + state.paned.pack1 (scroll, true, false); + state.paned.pack2 (state.scrolled_window, true, false); + + // Set initial position to 50/50 split + state.paned.position = scroll.get_allocated_width () / 2; + + // Add the paned widget back to the parent + if (scroll_parent is Gtk.Grid) { + ((Gtk.Grid) scroll_parent).add (state.paned); + } else if (scroll_parent is Gtk.Box) { + ((Gtk.Box) scroll_parent).pack_start (state.paned, true, true, 0); + } + + state.paned.show_all (); + state.visible = true; + + // Update the preview content + update_preview (doc, state); + + // Connect to buffer changes for live updates + doc.source_view.buffer.changed.connect (() => { + on_text_changed (doc); + }); + } + + private void hide_preview (Scratch.Services.Document doc) { + var state = preview_states.get (doc); + if (state == null || !state.visible) { + return; + } + + if (state.paned == null || state.original_scroll == null) { + return; + } + + // Get the parent that contains the paned widget + var paned_parent = state.paned.parent; + if (paned_parent == null) { + return; + } + + // Remove the scroll window from the paned + state.paned.remove (state.original_scroll); + + // Remove the paned from its parent + paned_parent.remove (state.paned); + + // Add the scroll window back to its original parent + if (paned_parent is Gtk.Grid) { + ((Gtk.Grid) paned_parent).add (state.original_scroll); + } else if (paned_parent is Gtk.Box) { + ((Gtk.Box) paned_parent).pack_start (state.original_scroll, true, true, 0); + } + + state.visible = false; + state.paned = null; + } + + private uint update_timeout_id = 0; + private Scratch.Services.Document? pending_update_doc = null; + + private void on_text_changed (Scratch.Services.Document doc) { + // Debounce updates - wait 500ms after typing stops + if (update_timeout_id > 0) { + Source.remove (update_timeout_id); + } + + pending_update_doc = doc; + update_timeout_id = Timeout.add (500, () => { + if (pending_update_doc != null) { + var state = preview_states.get (pending_update_doc); + if (state != null && state.visible) { + update_preview (pending_update_doc, state); + } + } + update_timeout_id = 0; + pending_update_doc = null; + return false; + }); + } + + private void update_preview (Scratch.Services.Document doc, PreviewState state) { + var buffer = doc.source_view.buffer; + Gtk.TextIter start, end; + buffer.get_bounds (out start, out end); + var markdown_text = buffer.get_text (start, end, false); + + var html = markdown_to_html (markdown_text); + state.web_view.load_html (html, null); + } + + private string markdown_to_html (string markdown) { + var html = new StringBuilder (); + + // Add HTML header with GitHub-flavored styling + html.append (""" + + + + + + + +"""); + + // Simple markdown parsing + var lines = markdown.split ("\n"); + bool in_code_block = false; + string list_type = ""; // "ul" or "ol" + string code_lang = ""; + + foreach (var line in lines) { + var trimmed = line.strip (); + + // Code blocks + if (trimmed.has_prefix ("```")) { + if (in_code_block) { + html.append ("\n"); + in_code_block = false; + code_lang = ""; + } else { + // Close list if open + if (list_type != "") { + html.append ("\n"); + list_type = ""; + } + + code_lang = trimmed.substring (3).strip (); + html.append ("
");
+                    in_code_block = true;
+                }
+                continue;
+            }
+            
+            if (in_code_block) {
+                html.append (GLib.Markup.escape_text (line));
+                html.append ("\n");
+                continue;
+            }
+            
+            // Headers
+            if (trimmed.has_prefix ("#")) {
+                if (list_type != "") {
+                    html.append ("\n");
+                    list_type = "";
+                }
+                
+                if (trimmed.has_prefix ("######")) {
+                    html.append ("
").append (process_inline (trimmed.substring (6).strip ())).append ("
\n"); + } else if (trimmed.has_prefix ("#####")) { + html.append ("
").append (process_inline (trimmed.substring (5).strip ())).append ("
\n"); + } else if (trimmed.has_prefix ("####")) { + html.append ("

").append (process_inline (trimmed.substring (4).strip ())).append ("

\n"); + } else if (trimmed.has_prefix ("###")) { + html.append ("

").append (process_inline (trimmed.substring (3).strip ())).append ("

\n"); + } else if (trimmed.has_prefix ("##")) { + html.append ("

").append (process_inline (trimmed.substring (2).strip ())).append ("

\n"); + } else if (trimmed.has_prefix ("#")) { + html.append ("

").append (process_inline (trimmed.substring (1).strip ())).append ("

\n"); + } + } + // Horizontal rule + else if (trimmed == "---" || trimmed == "***" || trimmed == "___") { + if (list_type != "") { + html.append ("\n"); + list_type = ""; + } + html.append ("
\n"); + } + // Blockquote + else if (trimmed.has_prefix (">")) { + if (list_type != "") { + html.append ("\n"); + list_type = ""; + } + html.append ("

").append (process_inline (trimmed.substring (1).strip ())).append ("

\n"); + } + // Unordered list + else if (trimmed.has_prefix ("* ") || trimmed.has_prefix ("- ") || trimmed.has_prefix ("+ ")) { + if (list_type == "ol") { + html.append ("\n"); + list_type = ""; + } + if (list_type == "") { + html.append ("
    \n"); + list_type = "ul"; + } + html.append ("
  • ").append (process_inline (trimmed.substring (2))).append ("
  • \n"); + } + // Ordered list + else if (trimmed.length > 2 && trimmed[0].isdigit () && trimmed[1] == '.' && trimmed[2] == ' ') { + if (list_type == "ul") { + html.append ("
\n"); + list_type = ""; + } + if (list_type == "") { + html.append ("
    \n"); + list_type = "ol"; + } + html.append ("
  1. ").append (process_inline (trimmed.substring (3))).append ("
  2. \n"); + } + // Empty line + else if (trimmed == "") { + if (list_type != "") { + html.append ("\n"); + list_type = ""; + } + html.append ("
    \n"); + } + // Regular paragraph + else { + if (list_type != "") { + html.append ("\n"); + list_type = ""; + } + html.append ("

    ").append (process_inline (line)).append ("

    \n"); + } + } + + if (list_type != "") { + html.append ("\n"); + } + + html.append (""); + return html.str; + } + + private string process_inline (string text) { + var result = GLib.Markup.escape_text (text); + + // Bold **text** or __text__ + try { + var bold_regex = new Regex ("\\*\\*(.+?)\\*\\*"); + result = bold_regex.replace (result, -1, 0, "\\1"); + + var bold_regex2 = new Regex ("__(.+?)__"); + result = bold_regex2.replace (result, -1, 0, "\\1"); + + // Italic *text* or _text_ + var italic_regex = new Regex ("\\*(.+?)\\*"); + result = italic_regex.replace (result, -1, 0, "\\1"); + + var italic_regex2 = new Regex ("_(.+?)_"); + result = italic_regex2.replace (result, -1, 0, "\\1"); + + // Inline code `code` + var code_regex = new Regex ("`(.+?)`"); + result = code_regex.replace (result, -1, 0, "\\1"); + + // Links [text](url) + var link_regex = new Regex ("\\[(.+?)\\]\\((.+?)\\)"); + result = link_regex.replace (result, -1, 0, "\\1"); + + // Images ![alt](url) + var img_regex = new Regex ("!\\[(.+?)\\]\\((.+?)\\)"); + result = img_regex.replace (result, -1, 0, "\"\\1\""); + } catch (RegexError e) { + warning ("Regex error: %s", e.message); + } + + return result; + } + + public void deactivate () { + // Clean up all preview states + foreach (var doc in preview_states.keys) { + if (preview_states.get (doc).visible) { + hide_preview (doc); + } + } + preview_states.clear (); + + if (toolbar != null && preview_button != null && preview_button.parent != null) { + toolbar.remove (preview_button); + } + + if (update_timeout_id > 0) { + Source.remove (update_timeout_id); + update_timeout_id = 0; + } + } +} + +[ModuleInit] +public void peas_register_types (TypeModule module) { + var objmodule = module as Peas.ObjectModule; + objmodule.register_extension_type (typeof (Scratch.Services.ActivatablePlugin), + typeof (Code.Plugins.MarkdownPreview)); +} diff --git a/plugins/markdown-preview/meson.build b/plugins/markdown-preview/meson.build new file mode 100644 index 0000000000..4421040778 --- /dev/null +++ b/plugins/markdown-preview/meson.build @@ -0,0 +1,34 @@ +module_name = 'markdown-preview' + +module_files = [ + 'markdown-preview.vala', +] + +module_deps = [ + codecore_dep, + dependency('webkit2gtk-4.1'), + dependency('gee-0.8'), +] + +shared_module( + module_name, + module_files, + dependencies: module_deps, + install: true, + install_dir: pluginsdir / module_name, +) + +custom_target(module_name + '.plugin_merge', + input: module_name + '.plugin', + output: module_name + '.plugin', + command : [msgfmt, + '--desktop', + '--keyword=Description', + '--keyword=Name', + '-d' + meson.project_source_root () / 'po' / 'plugins', + '--template=@INPUT@', + '-o@OUTPUT@', + ], + install : true, + install_dir: pluginsdir / module_name, +) diff --git a/plugins/meson.build b/plugins/meson.build index 906dcd0780..38111d213d 100644 --- a/plugins/meson.build +++ b/plugins/meson.build @@ -11,3 +11,4 @@ subdir('spell') subdir('vim-emulation') subdir('word-completion') subdir('fuzzy-search') +subdir('markdown-preview') From 3684cf7fa7d35d2d62db8a9c63685890c84e82fa Mon Sep 17 00:00:00 2001 From: CFFinch62 Date: Fri, 23 Jan 2026 15:14:44 -0500 Subject: [PATCH 2/2] Markdown preview fixes: app does not crash between preview activation/deactivations, imges render properly within the preiview pane, preiew and edited fiel split is now 50/50 at activation --- .../markdown-preview/markdown-preview.vala | 94 ++++++++++++++++--- ...{zh_Hant.po => zh_Hant (Case Conflict).po} | 0 ...{zh_Hant.po => zh_Hant (Case Conflict).po} | 0 po/{zh_Hant.po => zh_Hant (Case Conflict).po} | 0 4 files changed, 83 insertions(+), 11 deletions(-) rename po/extra/{zh_Hant.po => zh_Hant (Case Conflict).po} (100%) rename po/plugins/{zh_Hant.po => zh_Hant (Case Conflict).po} (100%) rename po/{zh_Hant.po => zh_Hant (Case Conflict).po} (100%) diff --git a/plugins/markdown-preview/markdown-preview.vala b/plugins/markdown-preview/markdown-preview.vala index a621d4c1d3..1566451871 100644 --- a/plugins/markdown-preview/markdown-preview.vala +++ b/plugins/markdown-preview/markdown-preview.vala @@ -24,6 +24,10 @@ public class Code.Plugins.MarkdownPreview : Peas.ExtensionBase, Scratch.Services Scratch.HeaderBar? toolbar = null; Gtk.ToggleButton? preview_button = null; + // Signal handler IDs + ulong hook_toolbar_id = 0; + ulong hook_document_id = 0; + // Store preview state per document private Gee.HashMap preview_states; @@ -37,6 +41,7 @@ public class Code.Plugins.MarkdownPreview : Peas.ExtensionBase, Scratch.Services public Gtk.Paned? paned = null; public Gtk.ScrolledWindow? original_scroll = null; public bool visible = false; + public ulong buffer_changed_id = 0; public PreviewState () { web_view = new WebKit.WebView (); @@ -73,6 +78,25 @@ public class Code.Plugins.MarkdownPreview : Peas.ExtensionBase, Scratch.Services } return false; }); + + // Handle navigation requests - Open links in external browser + web_view.decide_policy.connect ((decision, type) => { + if (type == WebKit.PolicyDecisionType.NAVIGATION_ACTION) { + var nav_decision = (WebKit.NavigationPolicyDecision) decision; + var action = nav_decision.navigation_action; + + if (action.get_navigation_type () == WebKit.NavigationType.LINK_CLICKED) { + try { + Gtk.show_uri (null, action.get_request ().uri, Gdk.CURRENT_TIME); + } catch (Error e) { + warning ("Failed to open URI: %s", e.message); + } + decision.ignore (); + return true; + } + } + return false; + }); } } @@ -88,12 +112,12 @@ public class Code.Plugins.MarkdownPreview : Peas.ExtensionBase, Scratch.Services preview_button.no_show_all = true; // Hook into toolbar - plugins.hook_toolbar.connect ((t) => { + hook_toolbar_id = plugins.hook_toolbar.connect ((t) => { toolbar = t; }); // Hook into document changes - plugins.hook_document.connect ((doc) => { + hook_document_id = plugins.hook_document.connect ((doc) => { if (current_source != null) { current_source.notify["language"].disconnect (on_language_changed); } @@ -191,6 +215,10 @@ public class Code.Plugins.MarkdownPreview : Peas.ExtensionBase, Scratch.Services warning ("Could not find parent of scroll window"); return; } + + // Capture width BEFORE removing from parent + int width = scroll.get_allocated_width (); + if (width <= 0) width = 600; // Fallback width // Store reference to original scroll window state.original_scroll = scroll; @@ -206,7 +234,7 @@ public class Code.Plugins.MarkdownPreview : Peas.ExtensionBase, Scratch.Services state.paned.pack2 (state.scrolled_window, true, false); // Set initial position to 50/50 split - state.paned.position = scroll.get_allocated_width () / 2; + state.paned.position = width / 2; // Add the paned widget back to the parent if (scroll_parent is Gtk.Grid) { @@ -222,9 +250,11 @@ public class Code.Plugins.MarkdownPreview : Peas.ExtensionBase, Scratch.Services update_preview (doc, state); // Connect to buffer changes for live updates - doc.source_view.buffer.changed.connect (() => { - on_text_changed (doc); - }); + if (state.buffer_changed_id == 0) { + state.buffer_changed_id = doc.source_view.buffer.changed.connect (() => { + on_text_changed (doc); + }); + } } private void hide_preview (Scratch.Services.Document doc) { @@ -246,6 +276,10 @@ public class Code.Plugins.MarkdownPreview : Peas.ExtensionBase, Scratch.Services // Remove the scroll window from the paned state.paned.remove (state.original_scroll); + // CRITICAL: Remove the preview scrolled window too, otherwise it gets destroyed + // when state.paned is removed/destroyed. + state.paned.remove (state.scrolled_window); + // Remove the paned from its parent paned_parent.remove (state.paned); @@ -256,6 +290,12 @@ public class Code.Plugins.MarkdownPreview : Peas.ExtensionBase, Scratch.Services ((Gtk.Box) paned_parent).pack_start (state.original_scroll, true, true, 0); } + // Disconnect signal + if (state.buffer_changed_id > 0) { + doc.source_view.buffer.disconnect (state.buffer_changed_id); + state.buffer_changed_id = 0; + } + state.visible = false; state.paned = null; } @@ -290,7 +330,21 @@ public class Code.Plugins.MarkdownPreview : Peas.ExtensionBase, Scratch.Services var markdown_text = buffer.get_text (start, end, false); var html = markdown_to_html (markdown_text); - state.web_view.load_html (html, null); + + // Fix relative paths by providing a base URI + string? base_uri = null; + if (doc.file != null) { + var parent = doc.file.get_parent (); + if (parent != null) { + base_uri = parent.get_uri (); + // Ensure URI ends with a slash so relative files resolve correctly + if (!base_uri.has_suffix ("/")) { + base_uri += "/"; + } + } + } + + state.web_view.load_html (html, base_uri); } private string markdown_to_html (string markdown) { @@ -550,13 +604,13 @@ public class Code.Plugins.MarkdownPreview : Peas.ExtensionBase, Scratch.Services var code_regex = new Regex ("`(.+?)`"); result = code_regex.replace (result, -1, 0, "\\1"); + // Images ![alt](url) - MUST BE BEFORE LINKS + var img_regex = new Regex ("!\\[(.+?)\\]\\((.+?)\\)"); + result = img_regex.replace (result, -1, 0, "\"\\1\""); + // Links [text](url) var link_regex = new Regex ("\\[(.+?)\\]\\((.+?)\\)"); result = link_regex.replace (result, -1, 0, "\\1"); - - // Images ![alt](url) - var img_regex = new Regex ("!\\[(.+?)\\]\\((.+?)\\)"); - result = img_regex.replace (result, -1, 0, "\"\\1\""); } catch (RegexError e) { warning ("Regex error: %s", e.message); } @@ -573,6 +627,24 @@ public class Code.Plugins.MarkdownPreview : Peas.ExtensionBase, Scratch.Services } preview_states.clear (); + // Disconnect main signal handlers + if (plugins != null) { + if (hook_toolbar_id > 0) { + plugins.disconnect (hook_toolbar_id); + hook_toolbar_id = 0; + } + if (hook_document_id > 0) { + plugins.disconnect (hook_document_id); + hook_document_id = 0; + } + } + + // Disconnect from current source + if (current_source != null) { + current_source.notify["language"].disconnect (on_language_changed); + current_source = null; + } + if (toolbar != null && preview_button != null && preview_button.parent != null) { toolbar.remove (preview_button); } diff --git a/po/extra/zh_Hant.po b/po/extra/zh_Hant (Case Conflict).po similarity index 100% rename from po/extra/zh_Hant.po rename to po/extra/zh_Hant (Case Conflict).po diff --git a/po/plugins/zh_Hant.po b/po/plugins/zh_Hant (Case Conflict).po similarity index 100% rename from po/plugins/zh_Hant.po rename to po/plugins/zh_Hant (Case Conflict).po diff --git a/po/zh_Hant.po b/po/zh_Hant (Case Conflict).po similarity index 100% rename from po/zh_Hant.po rename to po/zh_Hant (Case Conflict).po