[gnome-builder/wip/gtk4-port: 1387/1774] plugins/sphinx-preview: add sphinx preview plugin




commit 32036081d9d91bd42ed6c57baea21fd00c7406fb
Author: Christian Hergert <chergert redhat com>
Date:   Mon Jun 6 21:05:14 2022 -0700

    plugins/sphinx-preview: add sphinx preview plugin
    
    There is still more work to be done here for things like rst, but this
    gets the harder stuff working which is a sphinx compiler and transforms.
    
    Thanks to IdeHtmlGenerator, we can do this in a much cleaner fashion than
    we used to.

 meson_options.txt                                  |   1 +
 src/plugins/meson.build                            |   2 +
 src/plugins/sphinx-preview/gbp-sphinx-compiler.c   | 377 +++++++++++++++++++++
 src/plugins/sphinx-preview/gbp-sphinx-compiler.h   |  42 +++
 .../sphinx-preview/gbp-sphinx-html-generator.c     | 248 ++++++++++++++
 .../sphinx-preview/gbp-sphinx-html-generator.h     |  31 ++
 .../gbp-sphinx-preview-workspace-addin.c           | 326 ++++++++++++++++++
 .../gbp-sphinx-preview-workspace-addin.h           |  31 ++
 src/plugins/sphinx-preview/gtk/menus.ui            |  13 +
 src/plugins/sphinx-preview/meson.build             |  22 ++
 src/plugins/sphinx-preview/sphinx-preview-plugin.c |  48 +++
 .../sphinx-preview/sphinx-preview.gresource.xml    |   7 +
 src/plugins/sphinx-preview/sphinx-preview.plugin   |  10 +
 13 files changed, 1158 insertions(+)
---
diff --git a/meson_options.txt b/meson_options.txt
index 63d5b049a..77e07973e 100644
--- a/meson_options.txt
+++ b/meson_options.txt
@@ -78,6 +78,7 @@ option('plugin_rubocop', type: 'boolean')
 option('plugin_rust_analyzer', type: 'boolean')
 option('plugin_shellcmd', type: 'boolean')
 option('plugin_spellcheck', type: 'boolean')
+option('plugin_sphinx_preview', type: 'boolean')
 option('plugin_stylelint', type: 'boolean')
 option('plugin_sysprof', type: 'boolean')
 option('plugin_sysroot', type: 'boolean')
diff --git a/src/plugins/meson.build b/src/plugins/meson.build
index 48228513e..27f184111 100644
--- a/src/plugins/meson.build
+++ b/src/plugins/meson.build
@@ -117,6 +117,7 @@ subdir('rust-analyzer')
 #subdir('shellcmd')
 subdir('snippets')
 subdir('spellcheck')
+subdir('sphinx-preview')
 subdir('stylelint')
 #subdir('sublime')
 subdir('support')
@@ -194,6 +195,7 @@ status += [
   'Quick Highlight ...................... : @0@'.format(get_option('plugin_quick_highlight')),
   'Retab ................................ : @0@'.format(get_option('plugin_retab')),
   'rstcheck ............................. : @0@'.format(get_option('plugin_rstcheck')),
+  'Sphinx Preview (reStructuredText) .... : @0@'.format(get_option('plugin_sphinx_preview')),
   'Rubocop .............................. : @0@'.format(get_option('plugin_rubocop')),
   'Spellcheck ........................... : @0@'.format(get_option('plugin_spellcheck')),
   'Stylelint ............................ : @0@'.format(get_option('plugin_stylelint')),
diff --git a/src/plugins/sphinx-preview/gbp-sphinx-compiler.c 
b/src/plugins/sphinx-preview/gbp-sphinx-compiler.c
new file mode 100644
index 000000000..94a7f0143
--- /dev/null
+++ b/src/plugins/sphinx-preview/gbp-sphinx-compiler.c
@@ -0,0 +1,377 @@
+/* gbp-sphinx-compiler.c
+ *
+ * Copyright 2022 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY 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 <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "gbp-sphinx-compiler"
+
+#include "config.h"
+
+#include <glib/gstdio.h>
+
+#include <libide-io.h>
+#include <libide-threading.h>
+
+#include "gbp-sphinx-compiler.h"
+
+struct _GbpSphinxCompiler
+{
+  GObject  parent_instance;
+  GFile   *config_file;
+  GFile   *basedir;
+  GFile   *builddir;
+};
+
+G_DEFINE_FINAL_TYPE (GbpSphinxCompiler, gbp_sphinx_compiler, G_TYPE_OBJECT)
+
+enum {
+  PROP_0,
+  PROP_CONFIG_FILE,
+  N_PROPS
+};
+
+static GParamSpec *properties [N_PROPS];
+
+GbpSphinxCompiler *
+gbp_sphinx_compiler_new (GFile *config_file)
+{
+  g_return_val_if_fail (G_IS_FILE (config_file), NULL);
+
+  return g_object_new (GBP_TYPE_SPHINX_COMPILER,
+                       "config-file", config_file,
+                       NULL);
+}
+
+static gboolean
+remove_temporary_directory (GFile   *file,
+                            GError **error)
+{
+  g_autoptr(IdeDirectoryReaper) reaper = NULL;
+
+  g_assert (G_IS_FILE (file));
+  g_assert (g_file_is_native (file));
+
+  if (!g_file_query_exists (file, NULL))
+    return TRUE;
+
+  reaper = ide_directory_reaper_new ();
+  ide_directory_reaper_add_directory (reaper, file, 0);
+  if (!ide_directory_reaper_execute (reaper, NULL, error))
+    return FALSE;
+
+  if (!g_file_delete (file, NULL, error))
+    return FALSE;
+
+  return TRUE;
+}
+
+static GFile *
+create_temporary_directory (GError **error)
+{
+  g_autofree char *path = NULL;
+
+  if (!(path = g_dir_make_tmp ("gnome-builder-sphinx-XXXXXX", error)))
+    return NULL;
+
+  return g_file_new_for_path (path);
+}
+
+static void
+gbp_sphinx_compiler_constructed (GObject *object)
+{
+  GbpSphinxCompiler *self = (GbpSphinxCompiler *)object;
+  g_autoptr(GError) error = NULL;
+
+  g_assert (GBP_IS_SPHINX_COMPILER (self));
+
+  G_OBJECT_CLASS (gbp_sphinx_compiler_parent_class)->constructed (object);
+
+  if (self->config_file == NULL)
+    g_critical ("%s created without a config-file", G_OBJECT_TYPE_NAME (self));
+  else if (!(self->basedir = g_file_get_parent (self->config_file)))
+    g_critical ("Implausible GFile used as config-file");
+  else if (!(self->builddir = create_temporary_directory (&error)))
+    g_critical ("Failed to create build directory: %s", error->message);
+}
+
+static void
+gbp_sphinx_compiler_finalize (GObject *object)
+{
+  GbpSphinxCompiler *self = (GbpSphinxCompiler *)object;
+
+  if (self->builddir != NULL)
+    {
+      g_autoptr(GError) error = NULL;
+
+      if (!remove_temporary_directory (self->builddir, &error))
+        g_warning ("Failed to cleanup sphinx build directory: %s: %s",
+                   g_file_peek_path (self->builddir),
+                   error->message);
+
+      g_clear_object (&self->builddir);
+    }
+
+  g_clear_object (&self->config_file);
+  g_clear_object (&self->basedir);
+
+  G_OBJECT_CLASS (gbp_sphinx_compiler_parent_class)->finalize (object);
+}
+
+static void
+gbp_sphinx_compiler_get_property (GObject    *object,
+                                  guint       prop_id,
+                                  GValue     *value,
+                                  GParamSpec *pspec)
+{
+  GbpSphinxCompiler *self = GBP_SPHINX_COMPILER (object);
+
+  switch (prop_id)
+    {
+    case PROP_CONFIG_FILE:
+      g_value_set_object (value, self->config_file);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+gbp_sphinx_compiler_set_property (GObject      *object,
+                                  guint         prop_id,
+                                  const GValue *value,
+                                  GParamSpec   *pspec)
+{
+  GbpSphinxCompiler *self = GBP_SPHINX_COMPILER (object);
+
+  switch (prop_id)
+    {
+    case PROP_CONFIG_FILE:
+      self->config_file = g_value_dup_object (value);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+gbp_sphinx_compiler_class_init (GbpSphinxCompilerClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->constructed = gbp_sphinx_compiler_constructed;
+  object_class->finalize = gbp_sphinx_compiler_finalize;
+  object_class->get_property = gbp_sphinx_compiler_get_property;
+  object_class->set_property = gbp_sphinx_compiler_set_property;
+
+  properties [PROP_CONFIG_FILE] =
+    g_param_spec_object ("config-file",
+                         "Config File",
+                         "Config File",
+                         G_TYPE_FILE,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+gbp_sphinx_compiler_init (GbpSphinxCompiler *self)
+{
+}
+
+static void
+gbp_sphinx_compiler_load_cb (GObject      *object,
+                             GAsyncResult *result,
+                             gpointer      user_data)
+{
+  GFile *file = (GFile *)object;
+  g_autoptr(IdeTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+  g_autofree char *contents = NULL;
+  gsize len;
+
+  IDE_ENTRY;
+
+  g_assert (G_IS_FILE (file));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_TASK (task));
+
+  if (!g_file_load_contents_finish (file, result, &contents, &len, NULL, &error))
+    ide_task_return_error (task, g_steal_pointer (&error));
+  else
+    ide_task_return_pointer (task, g_steal_pointer (&contents), g_free);
+
+  IDE_EXIT;
+}
+
+static void
+gbp_sphinx_compiler_compile_cb (GObject      *object,
+                                GAsyncResult *result,
+                                gpointer      user_data)
+{
+  IdeSubprocess *subprocess = (IdeSubprocess *)object;
+  g_autoptr(IdeTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+  GCancellable *cancellable;
+  GFile *dest;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_SUBPROCESS (subprocess));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_TASK (task));
+
+  if (!ide_subprocess_wait_check_finish (subprocess, result, &error))
+    {
+      ide_task_return_error (task, g_steal_pointer (&error));
+      IDE_EXIT;
+    }
+
+  dest = ide_task_get_task_data (task);
+  cancellable = ide_task_get_cancellable (task);
+
+  g_assert (G_IS_FILE (dest));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  g_file_load_contents_async (dest,
+                              cancellable,
+                              gbp_sphinx_compiler_load_cb,
+                              g_steal_pointer (&task));
+
+  IDE_EXIT;
+}
+
+static char *
+replace_suffix (const char *str,
+                const char *suffix)
+{
+  const char *dot = strrchr (str, '.');
+  char *shortened;
+  char *ret;
+
+  if (dot == NULL)
+    return g_strdup (str);
+
+  shortened = g_strndup (str, dot - str);
+  ret = g_strconcat (shortened, suffix, NULL);
+  g_free (shortened);
+
+  return ret;
+}
+
+static void
+gbp_sphinx_compiler_purge_doctree (GbpSphinxCompiler *self,
+                                   const char        *relpath)
+{
+  g_autofree char *doctreepath = NULL;
+  g_autofree char *fullpath = NULL;
+
+  IDE_ENTRY;
+
+  g_assert (GBP_IS_SPHINX_COMPILER (self));
+  g_assert (relpath != NULL);
+
+  doctreepath = replace_suffix (relpath, ".doctree");
+  fullpath = g_build_filename (g_file_peek_path (self->builddir),
+                               ".doctrees",
+                               doctreepath,
+                               NULL);
+
+  g_unlink (fullpath);
+
+  IDE_EXIT;
+}
+
+void
+gbp_sphinx_compiler_compile_async (GbpSphinxCompiler   *self,
+                                   GFile               *file,
+                                   const char          *contents,
+                                   GCancellable        *cancellable,
+                                   GAsyncReadyCallback  callback,
+                                   gpointer             user_data)
+{
+  g_autoptr(IdeSubprocessLauncher) launcher = NULL;
+  g_autoptr(IdeSubprocess) subprocess = NULL;
+  g_autoptr(IdeTask) task = NULL;
+  g_autoptr(GError) error = NULL;
+  g_autoptr(GFile) tu = NULL;
+  g_autofree char *relpath = NULL;
+  g_autofree char *htmlpath = NULL;
+  const char *path;
+
+  IDE_ENTRY;
+
+  g_return_if_fail (GBP_IS_SPHINX_COMPILER (self));
+  g_return_if_fail (G_IS_FILE (file));
+  g_return_if_fail (g_file_has_prefix (file, self->basedir));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, gbp_sphinx_compiler_compile_async);
+
+  if (!g_file_is_native (file))
+    {
+      ide_task_return_unsupported_error (task);
+      IDE_EXIT;
+    }
+
+  path = g_file_peek_path (file);
+  relpath = g_file_get_relative_path (self->basedir, file);
+  htmlpath = replace_suffix (relpath, ".html");
+  tu = g_file_get_child (self->builddir, htmlpath);
+  ide_task_set_task_data (task, g_steal_pointer (&tu), g_object_unref);
+
+  gbp_sphinx_compiler_purge_doctree (self, relpath);
+
+  g_assert (path != NULL);
+  g_assert (G_IS_FILE (self->basedir));
+  g_assert (G_IS_FILE (self->builddir));
+  g_assert (g_file_peek_path (self->basedir) != NULL);
+  g_assert (g_file_peek_path (self->builddir) != NULL);
+
+  launcher = ide_subprocess_launcher_new (0);
+  ide_subprocess_launcher_push_args (launcher, IDE_STRV_INIT ("sphinx-build", "-Q", "-b", "html"));
+  ide_subprocess_launcher_push_argv (launcher, g_file_peek_path (self->basedir));
+  ide_subprocess_launcher_push_argv (launcher, g_file_peek_path (self->builddir));
+  ide_subprocess_launcher_push_argv (launcher, path);
+
+  if (!(subprocess = ide_subprocess_launcher_spawn (launcher, cancellable, &error)))
+    {
+      ide_task_return_error (task, g_steal_pointer (&error));
+      IDE_EXIT;
+    }
+
+  ide_subprocess_wait_check_async (subprocess,
+                                   cancellable,
+                                   gbp_sphinx_compiler_compile_cb,
+                                   g_steal_pointer (&task));
+
+  IDE_EXIT;
+}
+
+char *
+gbp_sphinx_compiler_compile_finish (GbpSphinxCompiler  *self,
+                                    GAsyncResult       *result,
+                                    GError            **error)
+{
+  g_return_val_if_fail (GBP_IS_SPHINX_COMPILER (self), NULL);
+  g_return_val_if_fail (IDE_IS_TASK (result), NULL);
+
+  return ide_task_propagate_pointer (IDE_TASK (result), error);
+}
diff --git a/src/plugins/sphinx-preview/gbp-sphinx-compiler.h 
b/src/plugins/sphinx-preview/gbp-sphinx-compiler.h
new file mode 100644
index 000000000..7826bd636
--- /dev/null
+++ b/src/plugins/sphinx-preview/gbp-sphinx-compiler.h
@@ -0,0 +1,42 @@
+/* gbp-sphinx-compiler.h
+ *
+ * Copyright 2022 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY 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 <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <gio/gio.h>
+
+G_BEGIN_DECLS
+
+#define GBP_TYPE_SPHINX_COMPILER (gbp_sphinx_compiler_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpSphinxCompiler, gbp_sphinx_compiler, GBP, SPHINX_COMPILER, GObject)
+
+GbpSphinxCompiler *gbp_sphinx_compiler_new            (GFile               *config_file);
+void               gbp_sphinx_compiler_compile_async  (GbpSphinxCompiler   *self,
+                                                       GFile               *file,
+                                                       const char          *contents,
+                                                       GCancellable        *cancellable,
+                                                       GAsyncReadyCallback  callback,
+                                                       gpointer             user_data);
+char              *gbp_sphinx_compiler_compile_finish (GbpSphinxCompiler  *self,
+                                                       GAsyncResult       *result,
+                                                       GError            **error);
+
+G_END_DECLS
diff --git a/src/plugins/sphinx-preview/gbp-sphinx-html-generator.c 
b/src/plugins/sphinx-preview/gbp-sphinx-html-generator.c
new file mode 100644
index 000000000..d02ac6e98
--- /dev/null
+++ b/src/plugins/sphinx-preview/gbp-sphinx-html-generator.c
@@ -0,0 +1,248 @@
+/* gbp-sphinx-html-generator.c
+ *
+ * Copyright 2022 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY 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 <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "gbp-sphinx-html-generator"
+
+#include "config.h"
+
+#include <libide-code.h>
+#include <libide-threading.h>
+
+#include "gbp-sphinx-compiler.h"
+#include "gbp-sphinx-html-generator.h"
+
+struct _GbpSphinxHtmlGenerator
+{
+  IdeHtmlGenerator   parent_instance;
+  GSignalGroup      *buffer_signals;
+  GbpSphinxCompiler *compiler;
+};
+
+enum {
+  PROP_0,
+  PROP_BUFFER,
+  PROP_COMPILER,
+  N_PROPS
+};
+
+G_DEFINE_FINAL_TYPE (GbpSphinxHtmlGenerator, gbp_sphinx_html_generator, IDE_TYPE_HTML_GENERATOR)
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+gbp_sphinx_html_generator_compile_cb (GObject      *object,
+                                      GAsyncResult *result,
+                                      gpointer      user_data)
+{
+  GbpSphinxCompiler *compiler = (GbpSphinxCompiler *)object;
+  g_autoptr(IdeTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+  g_autofree char *contents = NULL;
+  gsize len;
+
+  g_assert (GBP_IS_SPHINX_COMPILER (compiler));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_TASK (task));
+
+  if (!(contents = gbp_sphinx_compiler_compile_finish (compiler, result, &error)))
+    {
+      ide_task_return_error (task, g_steal_pointer (&error));
+      return;
+    }
+
+  len = strlen (contents);
+  ide_task_return_pointer (task,
+                           g_bytes_new_take (g_steal_pointer (&contents), len),
+                           g_bytes_unref);
+}
+
+static void
+gbp_sphinx_html_generator_generate_async (IdeHtmlGenerator    *generator,
+                                          GCancellable        *cancellable,
+                                          GAsyncReadyCallback  callback,
+                                          gpointer             user_data)
+{
+  GbpSphinxHtmlGenerator *self = (GbpSphinxHtmlGenerator *)generator;
+  g_autoptr(IdeBuffer) buffer = NULL;
+  g_autoptr(IdeTask) task = NULL;
+  GFile *file;
+
+  g_assert (GBP_IS_SPHINX_HTML_GENERATOR (self));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, gbp_sphinx_html_generator_generate_async);
+
+  if (self->compiler == NULL ||
+      !(buffer = g_signal_group_dup_target (self->buffer_signals)))
+    {
+      ide_task_return_new_error (task,
+                                 G_IO_ERROR,
+                                 G_IO_ERROR_CANCELLED,
+                                 "Operation was cancelled");
+      return;
+    }
+
+  file = ide_buffer_get_file (buffer);
+
+  gbp_sphinx_compiler_compile_async (self->compiler,
+                                     file,
+                                     NULL,
+                                     cancellable,
+                                     gbp_sphinx_html_generator_compile_cb,
+                                     g_steal_pointer (&task));
+}
+
+static GBytes *
+gbp_sphinx_html_generator_generate_finish (IdeHtmlGenerator  *generator,
+                                           GAsyncResult      *result,
+                                           GError           **error)
+{
+  g_assert (GBP_IS_SPHINX_HTML_GENERATOR (generator));
+  g_assert (IDE_IS_TASK (result));
+
+  return ide_task_propagate_pointer (IDE_TASK (result), error);
+}
+
+static gboolean
+file_to_base_uri (GBinding     *binding,
+                  const GValue *from,
+                  GValue       *to,
+                  gpointer      user_data)
+{
+  g_value_set_string (to, g_file_get_uri (g_value_get_object (from)));
+  return TRUE;
+}
+
+static void
+gbp_sphinx_html_generator_set_buffer (GbpSphinxHtmlGenerator *self,
+                                      IdeBuffer              *buffer)
+{
+  g_assert (GBP_IS_SPHINX_HTML_GENERATOR (self));
+  g_assert (!buffer || IDE_IS_BUFFER (buffer));
+
+  g_signal_group_set_target (self->buffer_signals, buffer);
+
+  if (IDE_IS_BUFFER (buffer))
+    g_object_bind_property_full (buffer, "file",
+                                 self, "base-uri",
+                                 G_BINDING_SYNC_CREATE,
+                                 file_to_base_uri,
+                                 NULL, NULL, NULL);
+}
+
+static void
+gbp_sphinx_html_generator_dispose (GObject *object)
+{
+  GbpSphinxHtmlGenerator *self = (GbpSphinxHtmlGenerator *)object;
+
+  g_clear_object (&self->buffer_signals);
+  g_clear_object (&self->compiler);
+
+  G_OBJECT_CLASS (gbp_sphinx_html_generator_parent_class)->dispose (object);
+}
+
+static void
+gbp_sphinx_html_generator_get_property (GObject    *object,
+                                        guint       prop_id,
+                                        GValue     *value,
+                                        GParamSpec *pspec)
+{
+  GbpSphinxHtmlGenerator *self = GBP_SPHINX_HTML_GENERATOR (object);
+
+  switch (prop_id)
+    {
+    case PROP_BUFFER:
+      g_value_take_object (value, g_signal_group_dup_target (self->buffer_signals));
+      break;
+
+    case PROP_COMPILER:
+      g_value_set_object (value, self->compiler);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+gbp_sphinx_html_generator_set_property (GObject      *object,
+                                        guint         prop_id,
+                                        const GValue *value,
+                                        GParamSpec   *pspec)
+{
+  GbpSphinxHtmlGenerator *self = GBP_SPHINX_HTML_GENERATOR (object);
+
+  switch (prop_id)
+    {
+    case PROP_BUFFER:
+      gbp_sphinx_html_generator_set_buffer (self, g_value_get_object (value));
+      break;
+
+    case PROP_COMPILER:
+      self->compiler = g_value_dup_object (value);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+gbp_sphinx_html_generator_class_init (GbpSphinxHtmlGeneratorClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  IdeHtmlGeneratorClass *generator_class = IDE_HTML_GENERATOR_CLASS (klass);
+
+  object_class->dispose = gbp_sphinx_html_generator_dispose;
+  object_class->get_property = gbp_sphinx_html_generator_get_property;
+  object_class->set_property = gbp_sphinx_html_generator_set_property;
+
+  generator_class->generate_async = gbp_sphinx_html_generator_generate_async;
+  generator_class->generate_finish = gbp_sphinx_html_generator_generate_finish;
+
+  properties [PROP_BUFFER] =
+    g_param_spec_object ("buffer", NULL, NULL,
+                         IDE_TYPE_BUFFER,
+                         (G_PARAM_READWRITE |
+                          G_PARAM_CONSTRUCT_ONLY |
+                          G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_COMPILER] =
+    g_param_spec_object ("compiler", NULL, NULL,
+                         GBP_TYPE_SPHINX_COMPILER,
+                         (G_PARAM_READWRITE |
+                          G_PARAM_CONSTRUCT_ONLY |
+                          G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+gbp_sphinx_html_generator_init (GbpSphinxHtmlGenerator *self)
+{
+  self->buffer_signals = g_signal_group_new (IDE_TYPE_BUFFER);
+
+  g_signal_group_connect_object (self->buffer_signals,
+                                 "changed",
+                                 G_CALLBACK (ide_html_generator_invalidate),
+                                 self,
+                                 G_CONNECT_SWAPPED);
+}
diff --git a/src/plugins/sphinx-preview/gbp-sphinx-html-generator.h 
b/src/plugins/sphinx-preview/gbp-sphinx-html-generator.h
new file mode 100644
index 000000000..a52de2268
--- /dev/null
+++ b/src/plugins/sphinx-preview/gbp-sphinx-html-generator.h
@@ -0,0 +1,31 @@
+/* gbp-sphinx-html-generator.h
+ *
+ * Copyright 2022 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY 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 <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <libide-webkit.h>
+
+G_BEGIN_DECLS
+
+#define GBP_TYPE_SPHINX_HTML_GENERATOR (gbp_sphinx_html_generator_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpSphinxHtmlGenerator, gbp_sphinx_html_generator, GBP, SPHINX_HTML_GENERATOR, 
IdeHtmlGenerator)
+
+G_END_DECLS
diff --git a/src/plugins/sphinx-preview/gbp-sphinx-preview-workspace-addin.c 
b/src/plugins/sphinx-preview/gbp-sphinx-preview-workspace-addin.c
new file mode 100644
index 000000000..803df3208
--- /dev/null
+++ b/src/plugins/sphinx-preview/gbp-sphinx-preview-workspace-addin.c
@@ -0,0 +1,326 @@
+/* gbp-sphinx-preview-workspace-addin.c
+ *
+ * Copyright 2022 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY 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 <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "gbp-sphinx-preview-workspace-addin"
+
+#include "config.h"
+
+#include <libide-code.h>
+#include <libide-editor.h>
+#include <libide-gui.h>
+#include <libide-webkit.h>
+
+#include "gbp-sphinx-compiler.h"
+#include "gbp-sphinx-html-generator.h"
+#include "gbp-sphinx-preview-workspace-addin.h"
+
+struct _GbpSphinxPreviewWorkspaceAddin
+{
+  GObject        parent_instance;
+  IdeWorkspace  *workspace;
+  GSignalGroup  *buffer_signals;
+  IdeEditorPage *editor_page;
+  GHashTable    *compilers;
+};
+
+static void live_preview_action (GbpSphinxPreviewWorkspaceAddin *self,
+                                 GVariant                       *params);
+
+IDE_DEFINE_ACTION_GROUP (GbpSphinxPreviewWorkspaceAddin, gbp_sphinx_preview_workspace_addin, {
+  { "sphinx-preview", live_preview_action },
+})
+
+static void
+gbp_sphinx_preview_workspace_addin_set_language (GbpSphinxPreviewWorkspaceAddin *self,
+                                                 const char                     *language_id)
+{
+  gboolean enabled;
+
+  g_assert (GBP_IS_SPHINX_PREVIEW_WORKSPACE_ADDIN (self));
+
+  IDE_TRACE_MSG ("Switching language-id to %s", language_id ? language_id : "NULL");
+
+  enabled = ide_str_equal0 (language_id, "rst");
+  gbp_sphinx_preview_workspace_addin_set_action_enabled (self, "sphinx-preview", enabled);
+}
+
+static void
+gbp_sphinx_preview_workspace_addin_page_changed (IdeWorkspaceAddin *addin,
+                                                 IdePage           *page)
+{
+  GbpSphinxPreviewWorkspaceAddin *self = (GbpSphinxPreviewWorkspaceAddin *)addin;
+  IdeBuffer *buffer = NULL;
+
+  IDE_ENTRY;
+
+  g_assert (GBP_IS_SPHINX_PREVIEW_WORKSPACE_ADDIN (self));
+  g_assert (!page || IDE_IS_PAGE (page));
+
+  self->editor_page = NULL;
+
+  /* Make sure the page is an editor page and a local file, as that
+   * is the only kind of files we can process with sphinx.
+   */
+  if (IDE_IS_EDITOR_PAGE (page))
+    {
+      GFile *file = ide_editor_page_get_file (IDE_EDITOR_PAGE (page));
+
+      if (g_file_is_native (file))
+        {
+          self->editor_page = IDE_EDITOR_PAGE (page);
+          buffer = ide_editor_page_get_buffer (self->editor_page);
+        }
+    }
+
+  g_signal_group_set_target (self->buffer_signals, buffer);
+
+  IDE_EXIT;
+}
+
+static void
+gbp_sphinx_preview_workspace_addin_notify_language_id (GbpSphinxPreviewWorkspaceAddin *self,
+                                                       GParamSpec                     *pspec,
+                                                       IdeBuffer                      *buffer)
+{
+  g_assert (GBP_IS_SPHINX_PREVIEW_WORKSPACE_ADDIN (self));
+  g_assert (IDE_IS_BUFFER (buffer));
+
+  gbp_sphinx_preview_workspace_addin_set_language (self, ide_buffer_get_language_id (buffer));
+}
+
+static void
+gbp_sphinx_preview_workspace_addin_bind (GbpSphinxPreviewWorkspaceAddin *self,
+                                         IdeBuffer                      *buffer,
+                                         GSignalGroup                   *signal_group)
+{
+  g_assert (GBP_IS_SPHINX_PREVIEW_WORKSPACE_ADDIN (self));
+  g_assert (IDE_IS_BUFFER (buffer));
+  g_assert (G_IS_SIGNAL_GROUP (signal_group));
+
+  gbp_sphinx_preview_workspace_addin_set_language (self, ide_buffer_get_language_id (buffer));
+}
+
+static void
+gbp_sphinx_preview_workspace_addin_unbind (GbpSphinxPreviewWorkspaceAddin *self,
+                                           GSignalGroup                   *signal_group)
+{
+  g_assert (GBP_IS_SPHINX_PREVIEW_WORKSPACE_ADDIN (self));
+  g_assert (G_IS_SIGNAL_GROUP (signal_group));
+
+  gbp_sphinx_preview_workspace_addin_set_language (self, NULL);
+}
+
+static void
+gbp_sphinx_preview_workspace_addin_load (IdeWorkspaceAddin *addin,
+                                         IdeWorkspace      *workspace)
+{
+  GbpSphinxPreviewWorkspaceAddin *self = (GbpSphinxPreviewWorkspaceAddin *)addin;
+
+  IDE_ENTRY;
+
+  g_assert (GBP_IS_SPHINX_PREVIEW_WORKSPACE_ADDIN (self));
+  g_assert (IDE_IS_WORKSPACE (workspace));
+
+  self->workspace = workspace;
+
+  self->compilers = g_hash_table_new_full (g_file_hash,
+                                           (GEqualFunc)g_file_equal,
+                                           g_object_unref,
+                                           g_object_unref);
+
+  self->buffer_signals = g_signal_group_new (IDE_TYPE_BUFFER);
+  g_signal_connect_object (self->buffer_signals,
+                           "bind",
+                           G_CALLBACK (gbp_sphinx_preview_workspace_addin_bind),
+                           self,
+                           G_CONNECT_SWAPPED);
+  g_signal_connect_object (self->buffer_signals,
+                           "unbind",
+                           G_CALLBACK (gbp_sphinx_preview_workspace_addin_unbind),
+                           self,
+                           G_CONNECT_SWAPPED);
+  g_signal_group_connect_object (self->buffer_signals,
+                                 "notify::language-id",
+                                 G_CALLBACK (gbp_sphinx_preview_workspace_addin_notify_language_id),
+                                 self,
+                                 G_CONNECT_SWAPPED);
+
+  gtk_widget_insert_action_group (GTK_WIDGET (workspace),
+                                  "sphinx-preview",
+                                  G_ACTION_GROUP (self));
+
+  IDE_EXIT;
+}
+
+static void
+gbp_sphinx_preview_workspace_addin_unload (IdeWorkspaceAddin *addin,
+                                           IdeWorkspace      *workspace)
+{
+  GbpSphinxPreviewWorkspaceAddin *self = (GbpSphinxPreviewWorkspaceAddin *)addin;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_SPHINX_PREVIEW_WORKSPACE_ADDIN (self));
+  g_assert (IDE_IS_WORKSPACE (workspace));
+
+  gtk_widget_insert_action_group (GTK_WIDGET (workspace), "sphinx-preview", NULL);
+
+  g_clear_pointer (&self->compilers, g_hash_table_unref);
+  g_clear_object (&self->buffer_signals);
+
+  self->editor_page = NULL;
+  self->workspace = NULL;
+
+  IDE_EXIT;
+}
+
+static void
+workspace_addin_iface_init (IdeWorkspaceAddinInterface *iface)
+{
+  iface->load = gbp_sphinx_preview_workspace_addin_load;
+  iface->unload = gbp_sphinx_preview_workspace_addin_unload;
+  iface->page_changed = gbp_sphinx_preview_workspace_addin_page_changed;
+}
+
+G_DEFINE_FINAL_TYPE_WITH_CODE (GbpSphinxPreviewWorkspaceAddin, gbp_sphinx_preview_workspace_addin, 
G_TYPE_OBJECT,
+                               G_IMPLEMENT_INTERFACE (IDE_TYPE_WORKSPACE_ADDIN, workspace_addin_iface_init)
+                               G_IMPLEMENT_INTERFACE (G_TYPE_ACTION_GROUP, 
gbp_sphinx_preview_workspace_addin_init_action_group))
+
+static void
+gbp_sphinx_preview_workspace_addin_class_init (GbpSphinxPreviewWorkspaceAddinClass *klass)
+{
+}
+
+static void
+gbp_sphinx_preview_workspace_addin_init (GbpSphinxPreviewWorkspaceAddin *self)
+{
+  gbp_sphinx_preview_workspace_addin_set_action_enabled (self, "sphinx-preview", FALSE);
+}
+
+static IdePage *
+open_sphinx_preview (GbpSphinxPreviewWorkspaceAddin *self,
+                     IdeBuffer                      *buffer,
+                     GFile                          *conf_py)
+{
+  g_autoptr(IdeHtmlGenerator) generator = NULL;
+  GbpSphinxCompiler *compiler;
+  IdeWebkitPage *page;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_SPHINX_PREVIEW_WORKSPACE_ADDIN (self));
+  g_assert (IDE_IS_BUFFER (buffer));
+  g_assert (G_IS_FILE (conf_py));
+  g_assert (self->compilers != NULL);
+
+  if (!(compiler = g_hash_table_lookup (self->compilers, conf_py)))
+    {
+      compiler = gbp_sphinx_compiler_new (conf_py);
+      g_hash_table_insert (self->compilers, g_file_dup (conf_py), compiler);
+    }
+
+  generator = g_object_new (GBP_TYPE_SPHINX_HTML_GENERATOR,
+                            "buffer", buffer,
+                            "compiler", compiler,
+                            NULL);
+  page = ide_webkit_page_new_for_generator (generator);
+
+  IDE_RETURN (IDE_PAGE (page));
+}
+
+static IdePage *
+open_rst_preview (GbpSphinxPreviewWorkspaceAddin *self,
+                  IdeBuffer                      *buffer)
+{
+  g_autoptr(IdeHtmlGenerator) generator = NULL;
+  IdeWebkitPage *page;
+
+  IDE_ENTRY;
+
+  g_assert (GBP_IS_SPHINX_PREVIEW_WORKSPACE_ADDIN (self));
+  g_assert (IDE_IS_BUFFER (buffer));
+
+  /* TODO: docutils translation with python in subprocess */
+  generator = ide_html_generator_new_for_buffer (GTK_TEXT_BUFFER (buffer));
+  page = ide_webkit_page_new_for_generator (generator);
+
+  IDE_RETURN (IDE_PAGE (page));
+}
+
+static void
+live_preview_action (GbpSphinxPreviewWorkspaceAddin *self,
+                     GVariant                       *params)
+{
+  g_autoptr(IdePanelPosition) position = NULL;
+  g_autoptr(IdeBuffer) buffer = NULL;
+  g_autoptr(GFile) workdir = NULL;
+  g_autoptr(GFile) parent = NULL;
+  IdeContext *context;
+  IdePage *page = NULL;
+  GFile *file;
+  guint column;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_SPHINX_PREVIEW_WORKSPACE_ADDIN (self));
+  g_assert (IDE_IS_WORKSPACE (self->workspace));
+  g_assert (IDE_IS_EDITOR_PAGE (self->editor_page));
+
+  context = ide_workspace_get_context (self->workspace);
+  workdir = ide_context_ref_workdir (context);
+  file = ide_editor_page_get_file (self->editor_page);
+  parent = g_file_get_parent (file);
+  buffer = g_signal_group_dup_target (self->buffer_signals);
+  position = ide_page_get_position (IDE_PAGE (self->editor_page));
+
+  if (!ide_panel_position_get_column (position, &column))
+    column = 0;
+
+  ide_panel_position_set_column (position, column + 1);
+  ide_panel_position_set_depth (position, 0);
+
+  while (parent != NULL &&
+         (g_file_equal (workdir, parent) || g_file_has_prefix (parent, workdir)))
+    {
+      g_autoptr(GFile) conf_py = g_file_get_child (parent, "conf.py");
+      g_autoptr(GFile) old_parent = NULL;
+
+      if (g_file_query_exists (conf_py, NULL))
+        {
+          /* Found our top-level sphinx directory */
+          page = open_sphinx_preview (self, buffer, conf_py);
+          break;
+        }
+
+      old_parent = parent;
+      parent = g_file_get_parent (old_parent);
+    }
+
+  if (page == NULL)
+    page = open_rst_preview (self, buffer);
+
+  ide_workspace_add_page (self->workspace, page, position);
+  panel_widget_raise (PANEL_WIDGET (page));
+
+  IDE_EXIT;
+}
diff --git a/src/plugins/sphinx-preview/gbp-sphinx-preview-workspace-addin.h 
b/src/plugins/sphinx-preview/gbp-sphinx-preview-workspace-addin.h
new file mode 100644
index 000000000..44f46e203
--- /dev/null
+++ b/src/plugins/sphinx-preview/gbp-sphinx-preview-workspace-addin.h
@@ -0,0 +1,31 @@
+/* gbp-sphinx-preview-workspace-addin.h
+ *
+ * Copyright 2022 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY 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 <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+#define GBP_TYPE_SPHINX_PREVIEW_WORKSPACE_ADDIN (gbp_sphinx_preview_workspace_addin_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpSphinxPreviewWorkspaceAddin, gbp_sphinx_preview_workspace_addin, GBP, 
SPHINX_PREVIEW_WORKSPACE_ADDIN, GObject)
+
+G_END_DECLS
diff --git a/src/plugins/sphinx-preview/gtk/menus.ui b/src/plugins/sphinx-preview/gtk/menus.ui
new file mode 100644
index 000000000..1dc905380
--- /dev/null
+++ b/src/plugins/sphinx-preview/gtk/menus.ui
@@ -0,0 +1,13 @@
+<?xml version="1.0"?>
+<interface>
+  <menu id="ide-editor-page-menu">
+    <section id="ide-editor-page-preview-section">
+      <item>
+        <attribute name="id">sphinx-preview-item</attribute>
+        <attribute name="label" translatable="yes">Open Preview…</attribute>
+        <attribute name="action">sphinx-preview.sphinx-preview</attribute>
+        <attribute name="hidden-when">action-disabled</attribute>
+      </item>
+    </section>
+  </menu>
+</interface>
diff --git a/src/plugins/sphinx-preview/meson.build b/src/plugins/sphinx-preview/meson.build
new file mode 100644
index 000000000..ccd02f713
--- /dev/null
+++ b/src/plugins/sphinx-preview/meson.build
@@ -0,0 +1,22 @@
+if get_option('plugin_sphinx_preview')
+
+if not get_option('webkit').enabled()
+  error('-Dwebkit=enabled is required for sphinx-preview plugin')
+endif
+
+plugins_sources += files([
+  'sphinx-preview-plugin.c',
+  'gbp-sphinx-compiler.c',
+  'gbp-sphinx-html-generator.c',
+  'gbp-sphinx-preview-workspace-addin.c',
+])
+
+plugin_sphinx_preview_resources = gnome.compile_resources(
+  'sphinx-preview-resources',
+  'sphinx-preview.gresource.xml',
+  c_name: 'gbp_sphinx_preview'
+)
+
+plugins_sources += plugin_sphinx_preview_resources
+
+endif
diff --git a/src/plugins/sphinx-preview/sphinx-preview-plugin.c 
b/src/plugins/sphinx-preview/sphinx-preview-plugin.c
new file mode 100644
index 000000000..858b0d7be
--- /dev/null
+++ b/src/plugins/sphinx-preview/sphinx-preview-plugin.c
@@ -0,0 +1,48 @@
+/* sphinx-preview-plugin.c
+ *
+ * Copyright 2022 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY 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 <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "sphinx-preview-plugin"
+
+#include "config.h"
+
+#include <libpeas/peas.h>
+
+#include <libide-gui.h>
+
+#include "gbp-sphinx-preview-workspace-addin.h"
+
+_IDE_EXTERN void
+_gbp_sphinx_preview_register_types (PeasObjectModule *module)
+{
+  const char *path;
+
+  if (!(path = g_find_program_in_path ("sphinx-build")))
+    {
+      /* We always have it in the Flatpak, so just complain for other types
+       * of installations which are incomplete.
+       */
+      g_debug ("sphinx-build not found in PATH. Refusing to register addins.");
+      return;
+    }
+
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_WORKSPACE_ADDIN,
+                                              GBP_TYPE_SPHINX_PREVIEW_WORKSPACE_ADDIN);
+}
diff --git a/src/plugins/sphinx-preview/sphinx-preview.gresource.xml 
b/src/plugins/sphinx-preview/sphinx-preview.gresource.xml
new file mode 100644
index 000000000..cb615b98c
--- /dev/null
+++ b/src/plugins/sphinx-preview/sphinx-preview.gresource.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+  <gresource prefix="/plugins/sphinx-preview">
+    <file>sphinx-preview.plugin</file>
+    <file preprocess="xml-stripblanks">gtk/menus.ui</file>
+  </gresource>
+</gresources>
diff --git a/src/plugins/sphinx-preview/sphinx-preview.plugin 
b/src/plugins/sphinx-preview/sphinx-preview.plugin
new file mode 100644
index 000000000..40e5f4233
--- /dev/null
+++ b/src/plugins/sphinx-preview/sphinx-preview.plugin
@@ -0,0 +1,10 @@
+[Plugin]
+Authors=Christian Hergert <christian hergert me>
+Builtin=true
+Copyright=Copyright © 2015-2022 Christian Hergert
+Depends=editorui;webkit;
+Description=Live preview of HTML documents
+Embedded=_gbp_sphinx_preview_register_types
+Module=sphinx-preview
+Name=HTML Preview
+X-Workspace-Kind=primary;editor;


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]